terminalCode Example

WorldPanel and ScreenPanel — HUD overlays and 3D world-space UI with Razor PanelComponent

calendar_today May 5, 2026 schedule ~1 min read person patrickjr verified 50

WorldPanel and ScreenPanel — UI in the World

s&box UI uses PanelComponent (Razor) attached to either a ScreenPanel (HUD overlay) or WorldPanel (3D world-space UI). The same .razor component works with both.

ScreenPanel — HUD Overlay

Renders UI as a 2D overlay on the screen. Use for health bars, crosshairs, inventory, etc.

CSHARP
// In the scene, add a ScreenPanel component to a GameObject,
// then add your PanelComponent to the same GO.

var go = Scene.CreateObject();
go.Name = "HUD";

go.Components.Create<ScreenPanel>();
go.Components.Create<MyHudComponent>();
RAZOR
@* MyHudComponent.razor *@
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent

<root>
    <div class="hud">
        <div class="health">HP: @Health</div>
    </div>
</root>

@code {
    [Property] public float Health { get; set; } = 100f;
    protected override int BuildHash() => System.HashCode.Combine( Health );
}

WorldPanel — 3D World-Space UI

Renders UI on a flat plane in the world. Use for nameplates, interactive signs, shop menus, etc.

CSHARP
var go = Scene.CreateObject();
go.Name = "Nameplate";
go.WorldPosition = npcHeadPosition;

var worldPanel = go.Components.Create<WorldPanel>();
worldPanel.PanelSize    = new Vector2( 200, 50 );
worldPanel.RenderScale  = 1f;
worldPanel.LookAtCamera = true;   // billboard toward camera

go.Components.Create<NameplateComponent>();

PanelComponent Lifecycle

PanelComponent is a Component, so it has OnStart, OnUpdate, etc. Panel (child panels) uses OnAfterTreeRender( bool firstTime ) and Tick() instead.
CSHARP
public sealed class MyHudComponent : PanelComponent
{
    protected override void OnUpdate()
    {
        // Force a UI rebuild when data changes
        StateHasChanged();
    }
}

BuildHash — Efficient Rebuilds

The panel only rebuilds its DOM when BuildHash() returns a different value than last frame:

CSHARP
protected override int BuildHash()
    => System.HashCode.Combine( Health, Armor, AmmoCount );

Two-Way Binding

RAZOR
<SliderEntry min="0" max="100" step="1" Value:bind=@Volume />

@code {
    public float Volume { get; set; } = 0.8f;
}

Accessing the Panel from PanelComponent

CSHARP
// PanelComponent wraps a Panel — access it via .Panel
Panel.Style.Opacity = 0.5f;
Was this helpful?