Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/facepunch/sbox-public/llms.txt

Use this file to discover all available pages before exploring further.

The s&box UI system is built on a CSS-like layout engine (Yoga) combined with a Razor-compatible render tree. You write panels in C# or .razor files, style them with SCSS, and attach them to your scene through ScreenPanel or WorldPanel components. This page explains how the system fits together and how to wire it up to your game.

Core concepts

The UI hierarchy has three layers:
  • Panel — the base building block. Every visible UI element is a Panel or a subclass of it.
  • RootPanel — a Panel that owns a layout pass and a command list for rendering.
  • PanelComponent / ScreenPanel / WorldPanel — scene components that connect panels to the engine’s render pipeline.

Panel

Panel is the base class for all UI elements. It manages layout, styling, events, and the Razor render tree.

Creating a panel in C#

var myPanel = new Panel();
myPanel.AddClass( "my-panel" );
myPanel.Style.Width = 200f;
myPanel.Style.Height = 100f;
myPanel.Parent = rootPanel;

Styling

Styles can be set programmatically or via SCSS loaded through StyleSheet.Load:
// Programmatic
panel.Style.BackgroundColor = Color.Red;
panel.Style.Opacity = 0.8f;

// Load stylesheet
panel.StyleSheet.Load( "/ui/mypanel.scss" );
Panels support CSS-like pseudo-classes that automatically flip based on interaction state:
Pseudo-classFlagWhen active
:hoverPseudoClass.HoverMouse is over the panel
:activePseudoClass.ActivePanel is pressed
:focusPseudoClass.FocusPanel has keyboard focus
:introPseudoClass.IntroPanel was just added (entry animation)
:outroPseudoClass.OutroPanel is being removed (exit animation)
bool hovered = panel.HasHovered;
panel.Switch( PseudoClass.Active, true );

Tick

Panel.Tick() is called every frame and is the equivalent of Update for UI elements. Override it to update panel state:
public class HealthBar : Panel
{
    public override void Tick()
    {
        Style.Width = Length.Percent( player.Health );
    }
}

Visibility and input

bool visible = panel.IsVisible;        // accounts for ancestors
bool visibleSelf = panel.IsVisibleSelf;

// Does this panel (or any child) want the mouse cursor shown?
bool wantsMouse = panel.WantsMouseInput();

Deferred invocation

// Fire once after a delay (cancelled if panel is deleted)
panel.Invoke( 2.0f, () => panel.Delete() );

// Fire once, cancelling any previous call with the same name
panel.InvokeOnce( "flash", 0.1f, () => panel.RemoveClass( "flash" ) );
panel.CancelInvoke( "flash" );

Coordinate helpers

Vector2 delta = panel.ScreenPositionToPanelDelta( Mouse.Position );
Vector2 local = panel.ScreenPositionToPanelPosition( Mouse.Position );
Vector2 screen = panel.PanelPositionToScreenPosition( new Vector2( 50, 50 ) );

Razor panels (PanelComponent)

The recommended way to build UI is through PanelComponent subclasses paired with .razor template files. The engine re-renders the Razor tree whenever BuildHash() returns a different value than the previous frame.
1

Create the component

Create a C# class that inherits from PanelComponent. Place a .razor file with the same name alongside it.
// MyHud.cs
public class MyHud : PanelComponent
{
    [Property] public int Score { get; set; }

    protected override int BuildHash()
        => HashCode.Combine( Score );
}
2

Write the Razor template

The .razor file describes the panel’s content using HTML-like syntax. The SCSS file (same name) applies styles.
@* MyHud.razor *@
<root>
    <div class="score-label">Score: @Score</div>
</root>
/* MyHud.razor.scss */
.score-label {
    font-size: 24px;
    color: white;
    position: absolute;
    top: 20px;
    left: 20px;
}
3

Add to the scene

Add a ScreenPanel component to a GameObject, then add your MyHud component to the same GameObject or a child of it. The engine wires the panel hierarchy automatically.

Triggering a re-render

Call StateHasChanged() to mark the tree dirty and force a rebuild on the next frame:
Score += 10;
StateHasChanged();

Lifecycle callbacks

MethodWhen called
OnTreeFirstBuilt()Once, after the first Razor render
OnTreeBuilt()After every subsequent render
OnHotloaded()After hot-reload
OnParentChanged()When the panel’s parent changes
LanguageChanged()When the game language switches

RootPanel

RootPanel is the layout root that every tree must have. You rarely create it directly — ScreenPanel and WorldPanel create one for you. Key members:
RootPanel root = ...;

root.PanelBounds     // Rect — the root's size on screen/in world
root.Scale           // float — UI scale factor
root.RenderedManually // bool — true if rendered outside the standard pass
root.IsWorldPanel    // bool — true for WorldPanel roots

// Render manually (requires RenderedManually = true)
root.RenderManual( opacity: 1.0f );
Scale defaults to screenHeight / 1080.0f, keeping UI consistent across resolutions.

ScreenPanel

ScreenPanel is a scene component that renders its child PanelComponent hierarchy as a flat screen-space HUD.

Properties

PropertyDefaultDescription
Opacity1.0Overall alpha of this panel layer.
Scale1.0Manual scale multiplier.
AutoScreenScaletrueAutomatically scale based on screen resolution.
ScaleStrategyConsistentHeightConsistentHeight keeps 1080p reference; FollowDesktopScaling uses OS DPI.
ZIndex100Render order relative to other ScreenPanel layers.
TargetCameranullIf set, renders only for this camera.

Wiring up a HUD

GameObject
├── ScreenPanel          (component)
└── MyHud                (PanelComponent subclass)
Add both to the same GameObject. ScreenPanel becomes the root, and MyHud attaches to it automatically because PanelComponent.EnsureParentPanel walks up the hierarchy looking for an IRootPanelComponent.
A scene can have multiple ScreenPanel components with different ZIndex values for layered HUDs (for example, a game HUD at 100 and a chat overlay at 200).

WorldPanel

WorldPanel is a scene component that renders its child PanelComponent hierarchy as a texture projected into 3D world space. It inherits from Renderer, so it participates in scene culling.

Properties

PropertyDefaultDescription
PanelSize512×512The pixel dimensions of the panel canvas.
RenderScale1.0Scales the physical size in world units.
LookAtCamerafalseRotates the panel to face the active camera each frame.
HorizontalAlignCenterLeft / Center / Right pivot.
VerticalAlignCenterTop / Center / Bottom pivot.
InteractionRange1000Maximum distance from which players can interact with the panel.

Example setup

GameObject (world position: some wall)
├── WorldPanel       (component, PanelSize = 512×256)
└── ShopMenu         (PanelComponent subclass)
The WorldPanel creates an internal Sandbox.UI.WorldPanel in the scene world, applies the GameObject’s transform, and renders all attached PanelComponent children into it.
// At runtime, find and interact with the world panel component
var wp = GameObject.Components.Get<WorldPanel>();
wp.LookAtCamera = true;
wp.InteractionRange = 200f;
World panels are rendered as 3D scene objects and are affected by camera render tags. Make sure your camera’s RenderExcludeTags does not exclude the GameObject’s tags.

Event handling

Panels fire strongly-typed mouse events. Override them in a Panel subclass or respond to them in a PanelComponent:
// In a Panel subclass
protected override void OnMouseDown( MousePanelEvent e )
{
    Log.Info( $"Clicked at {e.LocalPosition}" );
    e.StopPropagation();
}

// In a PanelComponent subclass
protected override void OnMouseDown( MousePanelEvent e )
{
    FireAction();
}
Available event overrides: OnMouseDown, OnMouseUp, OnMouseMove, OnMouseOver, OnMouseOut, OnMouseWheel.

Next steps

Rendering pipeline

Understand how CameraComponent and the Graphics class drive rendering.

Custom shaders

Write HLSL shaders and use ComputeShader from C#.

Build docs developers (and LLMs) love