Skip to main content
s&box UI is built on an HTML/CSS panel model. You write layout using Razor (.razor) files and style with SCSS. The engine renders panels using its own GPU-accelerated layout engine — no browser required.

Panel model

Every UI element is a Panel. Panels form a tree: a parent panel clips and positions its children. Layout follows a CSS flexbox model, and styling uses a subset of CSS/SCSS. The two root containers are:
ComponentDescription
ScreenPanelRenders panels as a 2D overlay on the screen.
WorldPanelRenders panels in 3D world space, attached to a GameObject.

ScreenPanel

Add a ScreenPanel component to a GameObject to create a 2D HUD or menu layer. All PanelComponent children of that GameObject (or its descendants) are parented to this root automatically.
PropertyDescription
OpacityTransparency of the entire panel [0–1].
ScaleUniform scale multiplier.
AutoScreenScaleAutomatically scale the UI based on screen resolution.
ScaleStrategyConsistentHeight (assumes 1080p height) or FollowDesktopScaling.
ZIndexRender order; higher values are drawn on top.
TargetCameraOptional camera to render against (for split-screen, etc.).
// The ScreenPanel component is configured in the Inspector.
// Access it from code if needed:
var sp = Components.Get<ScreenPanel>();
sp.Opacity = 0.9f;
sp.ZIndex  = 200;

ScaleStrategy

Scales the UI so it appears the same physical size regardless of resolution, by assuming the screen height is always 1080 units. Good for gameplay HUDs.

WorldPanel

Add a WorldPanel component to render UI anchored to a point in 3D space — useful for name tags, interactive terminals, or floating labels.
PropertyTypeDescription
PanelSizeVector2Canvas size in screen-space pixels.
RenderScalefloatOverall scale of the world panel geometry.
LookAtCameraboolRotate to always face the camera.
HorizontalAlignenumLeft, Center, or Right pivot.
VerticalAlignenumTop, Center, or Bottom pivot.
InteractionRangefloatMaximum distance at which the player can interact.
// Configure from the Inspector, or access at runtime:
var wp = Components.Get<WorldPanel>();
wp.PanelSize       = new Vector2( 800, 400 );
wp.LookAtCamera    = true;
wp.InteractionRange = 250f;
WorldPanel is a Renderer component. It creates a SceneObject in the world and renders the panel tree into a texture each frame.

PanelComponent

PanelComponent is the base class for all game-defined UI. Pair it with a .razor file of the same name — the engine automatically links them.

Creating a panel component

1

Create the C# class

Derive from PanelComponent in a new .cs file:
public sealed class HealthHud : PanelComponent
{
    [Property] public float Health { get; set; } = 100f;
    [Property] public float MaxHealth { get; set; } = 100f;

    protected override int BuildHash()
    {
        return HashCode.Combine( Health, MaxHealth );
    }
}
2

Create the Razor file

Create HealthHud.razor next to your .cs file:
@using Sandbox;
@inherits PanelComponent

<root>
    <div class="health-bar">
        <div class="health-fill" style="width: @FillPercent%"></div>
        <label class="health-text">@Health / @MaxHealth</label>
    </div>
</root>

@code {
    float FillPercent => (Health / MaxHealth * 100f).Clamp( 0, 100 );
}
3

Create the SCSS file

Create HealthHud.razor.scss for styling:
.health-bar {
    width: 300px;
    height: 24px;
    background-color: rgba(0, 0, 0, 0.6);
    border-radius: 4px;
    overflow: hidden;
    position: relative;
}

.health-fill {
    height: 100%;
    background-color: #e84040;
    transition-duration: 0.3s;
}

.health-text {
    position: absolute;
    width: 100%;
    text-align: center;
    color: white;
    font-size: 14px;
    line-height: 24px;
}
4

Add to a ScreenPanel

In the scene, create a GameObject with ScreenPanel, then add your HealthHud component as a child.

BuildHash and reactivity

The engine re-renders your Razor markup whenever BuildHash() returns a different value than the previous frame. Always include every piece of data your template depends on:
protected override int BuildHash()
{
    // Re-render whenever any of these change
    return HashCode.Combine( Health, IsAlive, StatusEffect );
}
You can also call StateHasChanged() to force an immediate re-render:
Health -= damage;
StateHasChanged();

Lifecycle callbacks

protected override void OnTreeFirstBuilt()
{
    // Called once, after the Razor tree is rendered for the first time
    Log.Info( "HUD is ready" );
}

protected override void OnTreeBuilt()
{
    // Called every time the tree is re-rendered
}

Mouse events

Override these methods to handle pointer interaction in your panel:
protected override void OnMouseDown( MousePanelEvent e )
{
    Log.Info( $"Mouse button {e.Button} pressed" );
}

protected override void OnMouseMove( MousePanelEvent e ) { }
protected override void OnMouseUp( MousePanelEvent e )   { }
protected override void OnMouseOver( MousePanelEvent e ) { }
protected override void OnMouseOut( MousePanelEvent e )  { }
protected override void OnMouseWheel( Vector2 delta )    { }

CSS styling

Panel styling uses a CSS-like language with flexbox layout. You can use .scss files for nesting and variables.
// Variables
$accent: #3474ec;
$bg-dark: rgba(0, 0, 0, 0.7);

.inventory-panel {
    display: flex;
    flex-direction: column;
    background-color: $bg-dark;
    padding: 16px;
    border-radius: 8px;
    width: 400px;

    .slot {
        display: flex;
        flex-direction: row;
        align-items: center;
        padding: 8px;
        border-radius: 4px;
        margin-bottom: 4px;

        &:hover {
            background-color: rgba(255, 255, 255, 0.1);
        }

        &.selected {
            border: 2px solid $accent;
        }

        .icon {
            width: 48px;
            height: 48px;
        }

        label {
            flex-grow: 1;
            font-size: 16px;
            color: white;
        }
    }
}

Loading a stylesheet manually

Panel.StyleSheet.Load( "ui/my_styles.scss" );

Razor markup reference

Razor panels support standard HTML-like elements plus custom components.
@* Conditional rendering *@
@if ( IsVisible )
{
    <div class="tooltip">@Message</div>
}

@* Loops *@
@foreach ( var item in Items )
{
    <div class="item">@item.Name</div>
}

@* Style binding *@
<div style="opacity: @Opacity; color: @TintColor.Hex;">Content</div>

@* Class conditionals *@
<div class="slot @(IsSelected ? "selected" : "")">Content</div>

@* Events *@
<button onclick=@OnButtonClicked>Click me</button>

@code {
    void OnButtonClicked()
    {
        Log.Info( "Button clicked!" );
    }
}

ScreenPanel vs WorldPanel

ScreenPanel

  • 2D overlay, always on screen
  • Scales with resolution
  • Use for HUDs, menus, crosshairs
  • Unaffected by world depth

WorldPanel

  • 3D placement in world space
  • Supports LookAtCamera
  • Use for name tags, in-world screens
  • Occluded by world geometry

Full HUD example

// HealthHud.cs
public sealed class HealthHud : PanelComponent
{
    PlayerController player;

    protected override void OnStart()
    {
        player = Scene.GetAllComponents<PlayerController>().FirstOrDefault();
    }

    float Health    => player?.Health    ?? 0;
    float MaxHealth => player?.MaxHealth ?? 100;
    int   Ammo      => player?.Ammo      ?? 0;

    protected override int BuildHash()
    {
        return HashCode.Combine( Health, MaxHealth, Ammo );
    }
}
@* HealthHud.razor *@
@inherits PanelComponent

<root>
    <div class="hud">
        <div class="health-section">
            <div class="bar-bg">
                <div class="bar-fill" style="width: @HealthPct%"></div>
            </div>
            <label>@Health.FloorToInt() HP</label>
        </div>

        <div class="ammo-section">
            <label class="ammo">@Ammo</label>
        </div>
    </div>
</root>

@code {
    float HealthPct => (Health / MaxHealth * 100f).Clamp( 0, 100 );
}
/* HealthHud.razor.scss */
.hud {
    position: absolute;
    bottom: 32px;
    left: 32px;
    display: flex;
    flex-direction: column;
    gap: 8px;
}

.bar-bg {
    width: 240px;
    height: 16px;
    background-color: rgba(0,0,0,0.6);
    border-radius: 8px;
    overflow: hidden;
}

.bar-fill {
    height: 100%;
    background-color: #44cc44;
    transition-duration: 0.2s;
}

.ammo {
    font-size: 32px;
    color: white;
    font-weight: bold;
}

Build docs developers (and LLMs) love