USS gradients (linear-gradient) and image gradients

I know this was raised a couple of years ago but I haven’t seen anything about it since or any response from the unity team. Is there any chance we could get an update to uss files that allow us to add gradients to background colors and images?

I’d really like to be able to do something like this: background-color: linear-gradient(90deg, rgba(28,28,28,1) 0%, rgba(42,42,42,1) 30%, rgba(71,70,70,1) 100%);

Hi @BetterLife_R ,

We plan to include USS gradient support in the future. However, it’s not a top priority at the moment.

The current workaround would be to use the Mesh API. Here’s a sample:

        class SolidQuad : VisualElement
        {
            public SolidQuad() { generateVisualContent += OnGenerateVisualContent; }

            static readonly Vertex[] k_Vertices = new Vertex[4];
            static readonly ushort[] k_Indices = { 0, 1, 2, 2, 3, 0 };

            void OnGenerateVisualContent(MeshGenerationContext mgc)
            {
                Rect r = contentRect;
                if (r.width < 0.01f || r.height < 0.01f)
                    return; // Skip rendering when too small.

                Color color = resolvedStyle.color;
                k_Vertices[0].tint = Color.black;
                k_Vertices[1].tint = Color.black;
                k_Vertices[2].tint = color;
                k_Vertices[3].tint = color;

                float left = 0;
                float right = r.width;
                float top = 0;
                float bottom = r.height;

                k_Vertices[0].position = new Vector3(left, bottom, Vertex.nearZ);
                k_Vertices[1].position = new Vector3(left, top, Vertex.nearZ);
                k_Vertices[2].position = new Vector3(right, top, Vertex.nearZ);
                k_Vertices[3].position = new Vector3(right, bottom, Vertex.nearZ);

                MeshWriteData mwd = mgc.Allocate(k_Vertices.Length, k_Indices.Length);
                mwd.SetAllVertices(k_Vertices);
                mwd.SetAllIndices(k_Indices);
            }
        }

I have been trying to replicate this and it works just fine with 4 vertices, but when i am trying to add more I do not get any mesh rendered or only some strange shapes

private readonly Vertex[] _vertices = new Vertex[5]; 
private readonly ushort[]_indices = {     0, 4, 1,     1, 4, 2,     2, 4, 3,     3, 4, 0 };   

[Inject] 
public FocusObscure(UIDocument canvas)
{    
_canvas = canvas;    
style.width = new Length(100, LengthUnit.Percent);    
style.height = new Length(100, LengthUnit.Percent);        
generateVisualContent += OnGenerateVisualContent;
}  

void OnGenerateVisualContent(MeshGenerationContext mgc)
{    
var r = contentRect;   
 if (r.width < 0.01f || r.height < 0.01f)        
return; // Skip rendering when too small.   
var left = 0f;
var right = r.width;
var top = 0f;
var bottom = r.height;
var centerX = r.width / 2;
var centerY = r.width / 2;

for (int i = 0; i < 4; i++)
         _vertices[i].tint = Color.black;        
_vertices[4].tint = new Color(0, 0, 0,255);        
_vertices[0].position = new Vector3(left, bottom, Vertex.nearZ);    
_vertices[1].position = new Vector3(left, top, Vertex.nearZ);    
_vertices[2].position = new Vector3(right, top, Vertex.nearZ);    
_vertices[3].position = new Vector3(right, bottom, Vertex.nearZ);        
_vertices[4].position = new Vector3(centerX, centerY, Vertex.nearZ);    
var mwd = mgc.Allocate(_vertices.Length, _indices.Length);    
mwd.SetAllVertices(_vertices);    
mwd.SetAllIndices(_indices);
}

Thank you for this solution. However it doesn’t support smooth edges when a border radius is specified.

Without gradient:
9802755--1407270--upload_2024-4-29_11-25-17.png

With gradient:
9802755--1407273--upload_2024-4-29_11-26-5.png

Are you using overflow:hidden to clip the corners? If so, then it’s going to be this way because it’s using the stencil to perform the clipping. You should add an edge around your mesh shape to fade the alpha out.

Yes, without overflow:hidden the container is just squared without any radius.

Hello! Changing the orientation of your indices should fix it:

private readonly ushort[ ]k_Indices = { 0, 1, 4, 1, 2, 4, 2, 3, 4, 3, 0, 4 };

Hi everyone, I want to use an image gradient. I have tried the SolidQuad script but looks like it ignores the image. I just wonder if there’s a way to use it with an image.

An alternative would be to use Painter2D:

static void GenerateVisualContent(MeshGenerationContext mgc)
{
    var p = mgc.painter2D;
    p.strokeGradient = new Gradient
    {
        alphaKeys = new[] { new GradientAlphaKey(1, 0), new GradientAlphaKey(1, 1) },
        colorKeys = new[] { new GradientColorKey(Color.black, 0), new GradientColorKey(Color.white, 1) }
    };
    p.lineWidth = 100;
    p.BeginPath();
    p.MoveTo(new Vector2(0, 50));
    p.LineTo(new Vector2(100, 50));
    p.Stroke();
}

If it is useful to someone.
Here is a more advanced version, where you can specify the number of colors, direction, intensity, but it only works through code.
Definitely works in Unity 6, I did not look at earlier versions.

using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UIElements;

public struct GradientStop {
    public readonly Color Color;
    public readonly float Location;

    public GradientStop(Color color, float location) {
        Color = color;
        Location = location;
    }
}

/// <summary>
/// Gradient fill
/// </summary>
/// <example>
/// Example of use:
/// <code>
/// var gradient = new GradientVisualElement(
///     stops: new List&lt;GradientStop&gt; {
///         new GradientStop(Color.white, 0.0f),
///         new GradientStop(Color.black, 1.0f)
///     },
///     startPoint: new Vector2(0.0f, 0.0f),
///     endPoint: new Vector2(1.0f, 1.0f)
/// ) {
///     style = {
///         position = Position.Absolute,
///         left = 0, top = 0,
///         bottom = 0, right = 0
///     }
/// };
/// _content.Add(gradient);
/// </code>
/// </example>
public class GradientVisualElement : VisualElement {
    
    // MARK: Piblic
    
    // List of gradient stops
    public List<GradientStop> Stops;
    
    // Starting point of the gradient in unit space (0-1)
    public Vector2 StartPoint;
    
    // Ending point of the gradient in unit space (0-1)
    public Vector2 EndPoint;

    // MARK: Private
    
    private static readonly Vertex[] KVertices = new Vertex[4];
    private static readonly ushort[] KIndices = { 0, 1, 2, 2, 3, 0 };
    
    // MARK: Life cycle
    
    public GradientVisualElement(List<GradientStop> stops, Vector2 startPoint, Vector2 endPoint) {
        Stops = stops ?? new List<GradientStop>();
        StartPoint = startPoint;
        EndPoint = endPoint;
        generateVisualContent += OnGenerateVisualContent;
    }

    // Empty constructor for cases where parameters are set later
    public GradientVisualElement() {
        Stops = new List<GradientStop>();
        StartPoint = new Vector2(0, 0);
        EndPoint = new Vector2(1, 1);
        generateVisualContent += OnGenerateVisualContent;
    }

    // MARK: Private funcs

    private void OnGenerateVisualContent(MeshGenerationContext mgc) {
        var rect = contentRect;
        if (rect.width < 0.01f || rect.height < 0.01f)
            // Skip rendering if size is too small
            return;

        // Convert startPoint and endPoint from unit space to rectangle coordinates
        var start = new Vector2(StartPoint.x * rect.width, StartPoint.y * rect.height);
        var end = new Vector2(EndPoint.x * rect.width, EndPoint.y * rect.height);
        var dir = end - start;

        // Positions of the vertices of the rectangle
        var positions = new [] {
            // Bottom left
            new Vector2(0, rect.height),
            
            // Top left
            new Vector2(0, 0),
            
            // Top right
            new Vector2(rect.width, 0),
            
            // Bottom right
            new Vector2(rect.width, rect.height)
        };

        // If the gradient direction is zero, we set one color
        if (dir.sqrMagnitude < 0.0001f) {
            var color = Stops.Count > 0 ? Stops[0].Color : Color.clear;
            for (var index = 0; index < KVertices.Length; index++) {
                KVertices[index].tint = color;
            }
        }
        else {
            // Calculate colors for each vertex
            for (var index = 0; index < KVertices.Length; index++) {
                var p = positions[index];
                var toP = p - start;
                var t = Vector2.Dot(toP, dir) / Vector2.Dot(dir, dir);
                t = Mathf.Clamp01(t); // We limit `t` in the range [0,1]
                KVertices[index].tint = GetColorAt(t);
            }
        }

        // Set the positions of the vertices
        KVertices[0].position = new Vector3(0, rect.height, Vertex.nearZ);
        KVertices[1].position = new Vector3(0, 0, Vertex.nearZ);
        KVertices[2].position = new Vector3(rect.width, 0, Vertex.nearZ);
        KVertices[3].position = new Vector3(rect.width, rect.height, Vertex.nearZ);

        var meshWriteData = mgc.Allocate(vertexCount: KVertices.Length, indexCount: KIndices.Length);
        meshWriteData.SetAllVertices(KVertices);
        meshWriteData.SetAllIndices(KIndices);
    }

    // Method for calculating color based on parameter `t`
    private Color GetColorAt(float t) {
        if (Stops.Count == 0) return Color.clear;
        if (Stops.Count == 1) return Stops[0].Color;

        // We assume that stops are sorted by location
        for (var index = 0; index < Stops.Count - 1; index++) {
            if (!(t >= Stops[index].Location) || !(t <= Stops[index + 1].Location))
                continue;
            
            var u = (t - Stops[index].Location) / (Stops[index + 1].Location - Stops[index].Location);
            return Color.Lerp(Stops[index].Color, Stops[index + 1].Color, u);
        }

        // If t is less than the first or greater than the last stop
        if (t < Stops[0].Location) return Stops[0].Color;
        if (t > Stops[^1].Location) return Stops[^1].Color;

        return Color.clear; // На всякий случай
    }
}

/// <summary>
/// Container with gradient fill and support for standard VisualElement styles (borders, rounding, etc.).
/// </summary>
/// <example>
/// Example of use:
/// <code>
/// var gradient = new GradientVisualElement(
///     stops: new List&lt;GradientStop&gt; {
///         new GradientStop(Color.white, 0.0f),
///         new GradientStop(Color.black, 1.0f)
///     },
///     startPoint: new Vector2(0.0f, 0.0f),
///     endPoint: new Vector2(1.0f, 1.0f)
/// );
///
/// var gradientContainer = new GradientContainer(gradientElement: gradient) {
///     style = {
///         position = Position.Absolute,
///         left = 0,
///         top = 0,
///         bottom = 0,
///         right = 0,
///         borderTopLeftRadius = 16f,
///         borderTopRightRadius = 16f,
///         borderBottomLeftRadius = 16f,
///         borderBottomRightRadius = 16f,
///     }
/// };
///
/// _content.Add(gradientContainer);
/// </code>
/// </example>
public class GradientContainer : VisualElement {
    
    private readonly GradientVisualElement _gradientElement;

    public GradientContainer(GradientVisualElement gradientElement) {
        _gradientElement = gradientElement;
        gradientElement.style.position = Position.Absolute;
        gradientElement.style.left = 0f;
        gradientElement.style.top = 0f;
        gradientElement.style.right = 0f;
        gradientElement.style.bottom = 0f;
        gradientElement.pickingMode = PickingMode.Ignore;
        style.overflow = Overflow.Hidden;
        Add(gradientElement);
    }

    public List<GradientStop> Stops {
        get => _gradientElement.Stops;
        set => _gradientElement.Stops = value;
    }

    public Vector2 StartPoint {
        get => _gradientElement.StartPoint;
        set => _gradientElement.StartPoint = value;
    }

    public Vector2 EndPoint {
        get => _gradientElement.EndPoint;
        set => _gradientElement.EndPoint = value;
    }
}

example

// If you only need a gradient without other styles, you can use only GradientVisualElement. If you need other styles, like border, it is better to use GradientContainer
var gradient = new GradientVisualElement(
    stops: new List<GradientStop> {
        new GradientStop(Color.white, 0.0f),
        new GradientStop(Color.black, 1.0f)
    },
    startPoint: new Vector2(0.0f, 0.5f),
    endPoint: new Vector2(1.0f, 0.5f)
){ 
    style = {
        position = Position.Absolute,
        left = 0f, top = 0f,
        right = 0f,bottom = 0f
    }
};

// OR `_content. Add(gradient);`
_content.Insert(index: 0, element: gradient);

I see that Unity 6.3 brings custom shaders and filters ( Unity - Manual: USS filter ).
So the fundamentals seems to be in place to implement linear-gradient in USS.

I am looking for a solution that works in USS and also in USS animations / transitions.
For example, I want to create a button with background linear-gradient that changes on hover or when disabled.

Will this be possible with Unity 6.3?

Damn, the lack of built-in gradients in a web-like tool at the end of 2025 is insane…

We do support two varieties of gradients:

Gradients like CSS linear-gradient and radial-gradient are planned for next year.

Please when you add this for background, also add it for border too

While it allows to solve problem and implement gradients, it still feels like workaround and of course by “lack of gradients” I mean linear-gradient in USS.

This is a common request. My understanding is that there isn’t an easy border-gradient CSS style to get inspiration from. We’ll probably have to come with a custom style for this one.

Let us know what CSS property/workflow you would use for border gradients.

So, here is how I have implemented a UI Toolkit element with both a “fill” gradient and another gradient for the border:

image

It actually had to be two distinct UI Elements, the background (without border) and the border itself, which is a child of the background object.

The border exposes these properties

        private static readonly CustomStyleProperty<string> GradientDirectionProperty = new("--gradient-direction");
        private static readonly CustomStyleProperty<Color> GradientFromProperty = new("--border-from");
        private static readonly CustomStyleProperty<Color> GradientToProperty = new("--border-to");

and then I create four narrow rectangles where the border should be.

One thing I would also absolutely like you to consider including in a future release is allowing us to re-use style properties such as background-color or border-color for custom visual appearances. As you see there I have had to create new ones just for these borders.

Because I think that if you specify the uss background and border-color properties, then the element will render a rectangle element with those colors. But it is not possible to override the default appearance while still specifying those properties, hence why I had to create new properties so I would not trigger the rendering of the default appearance.

Now, imagine I want to render an element that has a specific shape, like an octagon. I cannot use background and border-color otherwise it will trigger the rendering of the default rectangle and I would need to introduce yet more custom uss properties like “octagon-background-color” and “octagon-border-color”. If instead it used the default background-color I could keep using the same color classes I have for the rest of the interface, which I think is what happens if you use polygon-clip in “real” CSS.

Just +1ing the gradient feature… I had my yearly thought “what if I converted my UI to use UI Toolkit? let’s start with my titlescreen” and the literal first thing I had to do was make a gradient background for my title buttons.

It is crazy this is being pushed at all without a simple gradient

Hi, I found this article that describes how to make gradient borders in css3. It might help.