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>.