Test

arrow_backAll guides
Intermediate·Game Development·25 min

Working with UI (Razor)

Build in-game UI using Razor components. Create HUDs, menus, and interactive panels.

s&box UI is built from Panels — C# objects that lay out using a flexbox stylesheet engine. You can construct them entirely in code, but most games use Razor files (.razor) that mix HTML-like markup with inline C#, similar to ASP.NET Blazor. Panels render natively (there is no actual HTML engine under the hood), which makes them fast and well-integrated with the scene system.

Panel, PanelComponent, ScreenPanel, WorldPanel

Panel is the base UI building block (any node in the UI tree). PanelComponent is a Component that owns a root Panel and is added to a GameObject — it's how UI gets into the scene. To actually draw the UI, the GameObject also needs either a ScreenPanel component (for HUDs and full-screen menus, drawn in 2D over the camera) or a WorldPanel component (for 3D in-world UI like signs and health bars). One PanelComponent feeds one ScreenPanel or one WorldPanel.

Make a Razor panel

In the editor, right-click a folder under Code/ and choose 'New Razor Panel Component'. You'll get a paired .razor file (markup + @code block) that inherits from PanelComponent. Anything inside <root> is your panel's children; everything in @code is normal C#. Add [Property] fields just like a regular Component and they show up in the Inspector. Override BuildHash() so the panel only re-renders when its inputs change — this is critical for performance.

@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent

<root>
    <div class="hud">
        <div class="hp">HP: @Health</div>
        <div class="score">@Score</div>
    </div>
</root>

@code
{
    [Property] public int Health { get; set; } = 100;
    [Property] public int Score { get; set; } = 0;

    protected override int BuildHash() => System.HashCode.Combine( Health, Score );
}

Style with paired .scss

If you create a file next to your panel with the same name plus .scss (e.g. Hud.razor and Hud.razor.scss) it is automatically loaded as that panel's stylesheet. You can also add an explicit [StyleSheet("main.scss")] attribute on a Panel class, write inline <style> blocks before or after <root>, or set styles in code via Panel.Style.Width = Length.Percent(50). The stylesheet syntax is SCSS with most CSS3 layout features — flexbox, grid is partial, transitions, hover, etc.

.hud {
    position: absolute;
    bottom: 32px;
    left: 32px;
    font-family: Poppins;
    color: white;
}

.hud .hp {
    font-size: 28px;
    font-weight: 700;
    text-shadow: 0 2px 4px rgba(0,0,0,0.6);
}

.hud .score {
    font-size: 20px;
    opacity: 0.85;
}

Compose Razor components and pass props

Make smaller .razor files that derive from Panel (the default for .razor files), then drop them into a parent panel like a tag: <HealthBar Value=@(player.Health) />. You can store a reference with <HealthBar @ref="_bar" /> and read/write its properties from C#. Use :bind for two-way binding (<SliderEntry Value:bind=@MyValue />). For repeating content use a Razor @foreach loop — re-render is gated by your BuildHash, so include the loop's source data in the hash.

Force a rebuild

By default the engine only rebuilds a panel's children when its BuildHash changes (or when the user mouses over a pointer-events panel). If you need to force a rebuild — for example after an async operation completes — call StateHasChanged() and the rebuild is queued for the next frame. Avoid building heavy logic in the markup itself; do it in @code and just read the result in <root>.

Further reading