Skip to main content
The s&box UI system is built around Panels. A Panel is a C# class that holds child panels, uses a flex-based layout system, and renders using stylesheets. You can build panels entirely in C# code, or use Razor (.razor) files that give you HTML/CSS-like syntax with embedded C#.

Creating a panel in C#

A basic panel creates child elements in its constructor and updates them in Tick():
public class MyPanel : Panel
{
    public Label MyLabel { get; set; }

    public MyPanel()
    {
        MyLabel = new Label();
        MyLabel.Parent = this;
    }

    public override void Tick()
    {
        MyLabel.Text = $"{Time.Now}";
    }
}

Adding a panel to the hierarchy

Add a panel to any other panel by setting its Parent:
var myPanel = new MyPanel();
myPanel.Parent = this;
In a Razor file, add it as an element:
<MyPanel />

Displaying UI on screen or in the world

To draw UI you need a PanelComponent as the root. Add a PanelComponent to any GameObject that also has a ScreenPanel or WorldPanel component — it will render to whichever one is present.
public sealed class MyRootPanel : PanelComponent
{
    MyPanel myPanel;

    protected override void OnTreeFirstBuilt()
    {
        base.OnTreeFirstBuilt();

        myPanel = new MyPanel();
        myPanel.Parent = Panel;
    }
}
PanelComponent is not itself a Panel. Access its root panel through the Panel property — for example, Panel.Style.Left = Length.Auto instead of Style.Left = Length.Auto.

Scaling

ScreenPanel automatically scales all UI to a 1080p target height by default. You can disable or change this scaling target in the component’s settings in the editor.

Razor panels

Razor panels let you write UI using HTML-like markup with embedded C#. The output is the same panel hierarchy — Razor is just a more convenient syntax.

Creating a PanelComponent with Razor

In the editor, create a new component and choose New Razor Panel Component:
@using Sandbox;
@using Sandbox.UI;
@inherits PanelComponent

<root>
    <div class="title">@MyStringValue</div>
</root>

@code
{
    [Property, TextArea] public string MyStringValue { get; set; } = "Hello World!";

    protected override int BuildHash() => System.HashCode.Combine( MyStringValue );
}
Everything inside <root> is treated as HTML. Use @ to inject C# expressions or blocks:
<root>
  @foreach ( var player in Player.All )
  {
    <div class="player">
      <label>@player.Name</label>
      @if ( player.IsDead )
      {
        <img src="ui/skull.png" />
      }
    </div>
  }
</root>
If you omit the <root> element, all elements become direct children of the panel’s root automatically.

BuildHash

The panel only rebuilds its content when BuildHash() returns a different value than the previous frame. Include every value that affects the UI output:
protected override int BuildHash() => System.HashCode.Combine( Health, Armor );
To force an immediate rebuild, call StateHasChanged(). This queues a rebuild for the next frame. A panel also rebuilds automatically when the cursor enters, exits, or clicks it — if pointer-events is enabled on that panel.

Creating a child panel in Razor

Child panels inherit from Panel instead of PanelComponent. Create a .razor file that inherits Panel by default:
@using Sandbox;
@using Sandbox.UI;

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

@code
{
    public int Health { get; set; } = 100;
    public int Armor  { get; set; } = 100;

    protected override int BuildHash() => System.HashCode.Combine( Health, Armor );
}
Use it in a parent panel:
<MyChildPanel />
Pass values directly as attributes:
<MyChildPanel Health=@(30) Armor=@(75) />

Storing a panel reference

Use @ref to hold a typed reference to a child panel:
<root>
  <MyChildPanel @ref="PanelReference" />
</root>

@code
{
    MyChildPanel PanelReference { get; set; }

    protected override void OnStart()
    {
        PanelReference.Health = Player.Local.Health;
        PanelReference.Armor  = Player.Local.Armor;
    }
}

Two-way binds

Bind a property to a control so changes flow in both directions. Use :bind after the attribute name:
<SliderEntry min="0" max="100" step="1" Value:bind=@IntValue></SliderEntry>

@code
{
    public int IntValue { get; set; } = 32;
}
The slider updates when IntValue changes, and IntValue updates when the slider changes.

Panel vs PanelComponent differences

Since Panel is not a Component, it does not have OnStart(), OnUpdate(), and so on. Use OnAfterTreeRender(bool firstTime) and Tick() instead. You cannot use <MyPanelComponent /> inside another panel or PanelComponent — PanelComponents must be added to a GameObject with a ScreenPanel or WorldPanel.

Styling panels

Stylesheets

Place a .scss file alongside your .razor file with the same base name and it is automatically included:
Health.razor
Health.razor.scss
To load a stylesheet from a different location, add the attribute to your panel class:
[StyleSheet("main.scss")]
Import other stylesheets from within a stylesheet:
@import "buttons.scss";

Inline styles

Apply styles directly on elements in markup:
<label style="color: red">DANGER!</label>
<div class="progress-bar">
  <div class="fill" style="width: @(Progress * 100f)%"></div>
</div>

Style block in Razor

Add a <style> block before or after <root>:
<style>
  MyPanel {
    width: 100%;
    height: 100%;
  }
  .hp    { color: red; }
  .armor { color: blue; }
</style>

Styling in C# code

Modify a panel’s Style property directly from Tick() or OnUpdate():
myPanel.Style.Width = Length.Percent( Progress * 100f );

HUD painter

For simple, non-interactive HUD elements, you can draw directly onto the camera’s HUD each frame without creating any panels:
protected override void OnUpdate()
{
    if ( Scene.Camera is null ) return;

    var hud = Scene.Camera.Hud;

    hud.DrawRect( new Rect( 300, 300, 10, 10 ), Color.White );
    hud.DrawLine( new Vector2( 100, 100 ), new Vector2( 200, 200 ), 10, Color.White );
    hud.DrawText( new TextRendering.Scope( "Hello!", Color.Red, 32 ), Screen.Width * 0.5f );
}
Use HudPainter for simple indicators and overlays. It is more efficient than full panels because there is no layout, stylesheet processing, or interactivity overhead.

Localization

Any displayed string that starts with # is treated as a localization token and replaced with the user’s language string automatically:
<label>#spawnmenu.props</label>
Define tokens in a JSON file under Localization/<language-code>/:
{
  "menu.helloworld": "Hello World",
  "spawnmenu.props": "Models",
  "spawnmenu.tools": "Tools"
}
LanguageCode
Arabicar
Bulgarianbg
Simplified Chinesezh-cn
Traditional Chinesezh-tw
Czechcs
Danishda
Dutchnl
Englishen
Finnishfi
Frenchfr
Germande
Greekel
Hungarianhu
Italianit
Japaneseja
Koreanko
Norwegianno
Pirateen-pt
Polishpl
Portuguesept
Portuguese (Brazil)pt-br
Romanianro
Russianru
Spanish (Spain)es
Spanish (Latin America)es-419
Swedishsv
Thaith
Turkishtr
Ukrainianuk
Vietnamesevn