Skip to content

Typesafe C# authoring for Unity UI Toolkit Stylesheets

License

Notifications You must be signed in to change notification settings

TomMoore515/TypeUSS

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

11 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

TypeUSS

Typesafe C# authoring for Unity UI Toolkit Stylesheets

Write your UI Toolkit styles in C# with full IntelliSense and compile-time safety, then generate standard .uss files automatically. Get the best of both worlds: the developer experience of C# and the runtime performance of USS.

TypeUSS Unity C# USS

Note

The code contained in this package is from my personal Unity project Automancer, while we are using it in production, that does not mean it is perfect or stable - this package should be considered experimental and the API may change without warning.

Overview

Unity UI Toolkit forces you to choose between:

Approach Type Safety IntelliSense Performance
Inline C# ✅ ✅ ❌ Poor (per-element allocation)
USS files ❌ ❌ ✅ High (native UI Toolkit)

TypeUSS lets you author styles in C# and generates USS automatically:

// ❌ Before: No IntelliSense, typos compile fine, painful to maintain
.chat-container {
    widht: 400px;  /* Typo - no error until runtime */
    background-color: rgba(0, 0, 0, 0.7);
}
// âś… After: Full type safety, autocomplete, can share files with C# based component definitions
[GenerateUSS("UI/Generated/Chat.uss")]
public static class ChatStyles
{
    public static readonly Selector Container = Sel.Class("chat-container");
    
    public static readonly TypeStyle ContainerStyle = Container.Style(s => s
        .Width(400)                              // Autocomplete works!
        .BackgroundColor(new Color(0, 0, 0, 0.7f)));
}

// Usage - typesafe class names
container.AddToClassList(ChatStyles.Container);  // No magic strings

Generates standard USS files that Unity loads normally. Zero runtime overhead.

Installation

Manual Installation (recommended)

Copy the TypeUSS folder into your project's Assets/Scripts/ directory.

We recommend manual installation so you can easily add additional custom property wrappers as needed. TypeUSS doesn't wrap every USS property yet, just the most common ones. If you add new properties please consider contributing them!

Quick Start

1. Define your styles

using UnityEngine;
using TypeUSS;

[GenerateUSS("UI/Generated/Button.uss")]
public static class ButtonStyles
{
    // Define selectors (class names)
    public static readonly Selector Button = Sel.Class("btn");
    
    // Define rules (selector + properties)
    public static readonly TypeStyle ButtonBase = Button.Style(s => s
        .Padding(8, 16)
        .BorderRadius(4)
        .BackgroundColor(new Color(0.3f, 0.3f, 0.3f))
        .Color(Color.white));
    
    // Hover state
    public static readonly TypeStyle ButtonHover = Button.Hover().Style(s => s
        .BackgroundColor(new Color(0.4f, 0.4f, 0.4f)));
}

2. USS is generated automatically

When the Unity project recompiles, TypeUSS generates the '.uss' file in the location specified in their [GenerateUSS] attribute. You can also manually regenerate via TypeUSS → Regenerate All USS in the menu bar.

/* Auto-generated by TypeUSS - Do not edit manually */

.btn {
    padding: 8px 16px;
    border-radius: 4px;
    background-color: rgb(77, 77, 77);
    color: rgb(255, 255, 255);
}

.btn:hover {
    background-color: rgb(102, 102, 102);
}

3. Use in your UI toolkit elements

var button = new Button { text = "Click me" };
button.AddToClassList(ButtonStyles.Button);        // Typesafe class name

Features

Selectors

// Class selector: .my-class
Sel.Class("my-class")

// ID selector: #my-id
Sel.Id("my-id")

// Type selector: Button
Sel.Type<Button>()
Sel.Type("Button")

// Universal selector: *
Sel.All

// Escape hatch for complex selectors
Sel.Raw("#unity-text-input")

Pseudo-classes

Button.Hover()      // .btn:hover
Button.Active()     // .btn:active
Button.Focus()      // .btn:focus
Button.Enabled()    // .btn:enabled
Button.Disabled()   // .btn:disabled
Button.Checked()    // .btn:checked

// Chain them
Button.Hover().Focus()  // .btn:hover:focus

Combinators

// Child combinator: .parent > .child
Parent > Child

// Descendant combinator: .ancestor .descendant
Ancestor.Descendant(Child)

// Adjacent sibling: .a + .b
A + B

// Combine without space (multiple classes): .btn.primary
Button.And(Primary)
Button.And(Sel.Class("primary"))

// Add a modifier class: .btn.focused
Button.AddClass("focused")

Properties

TypeUSS wraps the most common USS properties with typesafe methods:

// Sizing with various length units
.Width(100)                        // 100px
.Width(Length.Percent(50))         // 50%
.Width(Length.Auto)                // auto
.MarginLeft(Length.Auto)           // auto (useful for push-right)

.Height(200)
.MinWidth(100)
.MaxHeight(500)

.FlexGrow(1)
.FlexDirection(FlexDirection.Row)
.JustifyContent(Justify.Center)
.AlignItems(Align.FlexStart)

.Padding(10)                   // all sides
.Padding(10, 20)               // vertical, horizontal
.Padding(10, 20, 10, 20)       // top, right, bottom, left
.Margin(10)

.Position(Position.Absolute)
.Top(0)
.Left(0)

.BackgroundColor(Color.black)
.Color(Color.white)
.BorderColor(Color.gray)

.BorderWidth(1)
.BorderRadius(4)

.Display(DisplayStyle.Flex)
.Visibility(Visibility.Hidden)
.Overflow(Overflow.Hidden)
.Opacity(0.5f)

// Typography
.FontSize(14)
.LetterSpacing(2)
.UnityTextAlign(TextAnchor.MiddleCenter)
.UnityFontStyleAndWeight(FontStyle.Bold)
.UnityFontDefinition(myFont)
.WhiteSpace(WhiteSpace.Normal)

// Background
.BackgroundImage(texture)
.BackgroundPosition(BackgroundPositionKeyword.Center, BackgroundPositionKeyword.Center)
.BackgroundSize(100, 100)
.BackgroundRepeat(Repeat.NoRepeat, Repeat.NoRepeat)

// Transform
.Rotate(45f)
.Scale(1.5f)
.TransformOrigin(Length.Percent(50), Length.Percent(50))

Escape Hatch

Don't stop coding to add a new property wrapper. Use .Prop() for anything not yet wrapped:

public static readonly TypeStyle HeaderStyle = Header.Style(s => s
    .Width(100)                              // Wrapped
    .BackgroundColor(Color.black)            // Wrapped
    .Prop("-unity-font-style", "bold")       // Escape hatch!
    .Prop("transition-duration", "0.2s"));   // Escape hatch!

When you notice you're using the same .Prop() call repeatedly, that's a signal to add a proper wrapper method.

Unity Internal Elements

Target Unity's internal element structure using Sel.Raw():

public static readonly Selector Input = Sel.Class("my-input");

// Style the internal text input element
public static readonly TypeStyle InputInner = (Input > Sel.Raw("#unity-text-input")).Style(s => s
    .BackgroundColor(Color.black)
    .Color(Color.white)
    .Padding(0, 5));

Automatic StyleSheet Registration

TypeUSS automatically finds MonoBehaviours in the open scene with a StyleSheet[] field named styleSheets and populates it with all generated stylesheets.

To automatically apply generated stylesheets to your UI, add a field to your UI controller:

public class UIManager : MonoBehaviour
{
    [SerializeField] private UIDocument document;
    [SerializeField] private StyleSheet[] styleSheets; // Auto-populated by TypeUSS
    
    void OnEnable()
    {
        foreach (var sheet in styleSheets)
            document.rootVisualElement.styleSheets.Add(sheet);
    }
}

Props Pattern Integration

TypeUSS works well with a React-inspired Props pattern for UI components. Co-locate styles at the top of each component file:

using System;
using UnityEngine;
using UnityEngine.UIElements;
using TypeUSS;

namespace MyGame.UI.Components
{
    // ============================================================
    // STYLES (co-located at top of file)
    // ============================================================

    [GenerateUSS("UI/Generated/MyComponent.uss")]
    public static class MyComponentStyles
    {
        public static readonly Selector Root = Sel.Class("my-component");
        public static readonly TypeStyle RootStyle = Root.Style(s => s
            .FlexDirection(FlexDirection.Row)
            .Padding(10));

        public static readonly Selector Label = Sel.Class("my-component__label");
        public static readonly TypeStyle LabelStyle = Label.Style(s => s
            .FontSize(14)
            .Color(Color.white));

        // Modifier class for focused state
        public static readonly TypeStyle LabelFocused = Label.And(Sel.Class("focused")).Style(s => s
            .Color(Color.yellow));
    }

    // ============================================================
    // PROPS (immutable configuration)
    // ============================================================

    public sealed record MyComponentProps(
        string Text,
        bool IsFocused = false,
        Action OnClick = null
    );

    // ============================================================
    // STATIC FUNCTIONAL COMPONENT
    // ============================================================

    public static class MyComponent
    {
        public static VisualElement Render(MyComponentProps props)
        {
            var root = new VisualElement();
            root.name = "my-component";
            root.AddToClassList(MyComponentStyles.Root);

            var label = new Label(props.Text);
            label.AddToClassList(MyComponentStyles.Label);
            label.EnableInClassList("focused", props.IsFocused);
            root.Add(label);

            root.RegisterCallback<ClickEvent>(_ => props.OnClick?.Invoke());
            return root;
        }

        public static void Update(VisualElement root, MyComponentProps props)
        {
            var label = root.Q<Label>();
            if (label != null)
            {
                label.text = props.Text;
                label.EnableInClassList("focused", props.IsFocused);
            }
        }
    }

    // ============================================================
    // WRAPPER COMPONENT (for imperative usage or IUpdatableComponent)
    // ============================================================

    public sealed class MyComponentWrapper : VisualElement
    {
        private MyComponentProps _props;

        public MyComponentWrapper(MyComponentProps props)
        {
            _props = props;
            var content = MyComponent.Render(props);
            Add(content);
        }

        public void UpdateProps(MyComponentProps props)
        {
            _props = props;
            MyComponent.Update(this[0], props);
        }
    }
}

Key Principles

Principle Description
Props are immutable sealed record with all configuration
Styles at top TypeUSS styles defined before component code
Render creates Render(props) returns new VisualElement tree
Update mutates Update(element, props) updates existing elements
BEM naming component, component__element, component--modifier
Modifier classes Dynamic state via EnableInClassList()

Unity Version Compatibility

TypeUSS was developed and tested with Unity 6.3 - it may work with earlier versions but I have not taken the time to verify compatibility.

License

MIT License - see LICENSE for details.

Contributing

Contributions welcome! Please feel free to submit issues and pull requests.

Adding a new property wrapper:

  1. Add the method to StyleBuilder.cs
  2. Add any necessary enum mappings to EnumExtensions
  3. Submit a PR

About

Typesafe C# authoring for Unity UI Toolkit Stylesheets

Resources

License

Stars

Watchers

Forks

Languages