styles

every blob takes init-only properties. you've already seen some of them: Padding, BackgroundColor, BorderRadius. this article covers how to keep them dry across many blobs, plus a full property reference for each blob.

the basics

set what you need:

new Container { Padding = 16, BackgroundColor = Color.White, BorderRadius = 8 }

nullable properties you don't set fall back to the engine default.

a few behavior rules worth knowing:

typography

most text style properties take the unit you'd expect. two do not: LineHeight and LetterSpacing are both typed Length? because the engine surfaces them that way, but the unit you pick changes how the value resolves, and the two are not symmetrical.

LineHeight resolves as:

unit result
Length.Percent(150) 1.5x of FontSize (the CSS multiplier)
Length.Pixels(24) 24 / FontSize, recomputed whenever FontSize changes
Length.Em(1.5f) silently treated as 1.0x (the engine's Em branch is missing)

LetterSpacing resolves as:

unit result
Length.Em(0.05f) 0.05 * FontSize, stays correct across font-size changes
Length.Pixels(2) 2 raw pixels

prefer Goo.Typography for both. the helpers pick the unit that scales correctly so you don't have to remember the table:

new Text("Heading")
{
    LineHeight    = Typography.LineHeightMultiplier(1.5f),   // routes through Length.Percent
    LetterSpacing = Typography.LetterSpacingEm(-0.02f),      // routes through Length.Em
}

LineHeightMultiplier goes through Length.Percent so the line-height tracks FontSize even when a child overrides it; passing Length.Em yourself would be silently clamped to 1.0x. LetterSpacingEm goes through Length.Em so the gap scales with the font. reach for Length.Pixels directly only when you want a fixed gap that does not scale: the right call for a designer mockup, the wrong call anywhere a child overrides FontSize.

reusing values

instead of repeating literal values everywhere, declare them once in a plain static class and reference them everywhere:

internal static class Style
{
    public static readonly Color CardBg     = Color.White;
    public static readonly Color TextDark   = Color.FromBytes(0x09, 0x0B, 0x10, 255);
    public static readonly Length CardPad   = 16f;
    public static readonly Length CardGap   = 8f;
    public const string  FontDisplay        = "Space Grotesk";
    public const int     WeightSemibold     = 600;
}

new Container
{
    BackgroundColor = Style.CardBg,
    FontColor       = Style.TextDark,
    Padding         = Style.CardPad,
    Gap             = Style.CardGap,
    FontFamily      = Style.FontDisplay,
}

this is the simplest way to stay dry: ordinary C# constants, resolved at compile time. for design values that swap at runtime, like light/dark themes or per-scope overrides, the tokens article covers Tokens.Scope, a dynamic ambient lookup. they are different tools: static constants for values that never change, Tokens for values that do.

Length accepts float implicitly (pixels), so static readonly Length CardPad = 16f works. int-typed properties (FontWeight, TransitionMs, Order, ZIndex) and string-typed ones (FontFamily, Cursor, animation names) can be const.

text style bundles

for the recurring four-property font block (family, size, weight, color), bundle the values in a TextStyle and apply it through Text.Style in one property. null fields in the bundle are no-ops:

static readonly TextStyle Label = new()
{
    FontFamily = "Vend Sans",
    FontSize   = 14f,
    FontWeight = 600,
    FontColor  = Color.White,
};

new Text( title ) { Style = Label }

this is a deliberate departure from the usual extract-as-function rule: a struct preset composes with per-call overrides while staying inside the init-only idiom.

first-declared wins. a per-field override must come BEFORE Style in the initializer; fields set after Style are ignored wherever the bundle already set that field:

new Text( label ) { FontColor = fg, Style = Label }   // fg wins
new Text( label ) { Style = Label, FontColor = fg }   // ignored: the bundle's color wins

the demo component layer ships theme-token presets in Code/Components/TextStyles.cs (TextStyles.Body, BodySm, BodySmSec, Label, Caption, Heading); each preset's XML doc lists exactly the four tokens it sets.

helpers

the most reusable unit is a function that returns a fully-styled blob, with children added after construction. extract it once and call it wherever you need it:

public class StyledCardUI : GooPanel<Container>
{
    protected override Container Build()
    {
        var col = new Container
        {
            FlexDirection = FlexDirection.Column,
            Padding       = 16,
            Gap           = 8,
            BackgroundColor = Color.White,
            BorderRadius  = 8,
            Width         = 320,
        };
        col.Children.Add(new Text("Hello"));
        col.Children.Add(new Text("World"));
        col.Children.Add(new Text("Styled card"));
        return col;
    }
}

one important caveat: only Container carries pooled storage. its children list comes from a per-frame pool that's alive only while Build() is running, so helpers that build a Container must be called from inside Build().

static readonly Container Header = new Container { ... }; crashes at static init, and keeping a Container reference between rebuilds crashes for the same reason: the children list goes back to the pool the moment Build() returns.

Text and Image have none of this, no children and no pool, so build them as static readonly fields.

the reusable unit is the function that builds the Container, not the Container itself.

nine-slice borders

a nine-slice border stretches one bitmap into a resizable frame: the four corners stay fixed while the four edges and the center stretch to fill. set BorderImageSource to a Texture and the four BorderImageWidth edges to the corner insets:

new Container
{
    BorderImageSource      = skin,   // a Texture, e.g. Texture.LoadFromFileSystem(path, FileSystem.Mounted)
    BorderImageWidthLeft   = 340f,   // corner inset, in the source image's own pixels
    BorderImageWidthTop    = 340f,
    BorderImageWidthRight  = 340f,
    BorderImageWidthBottom = 340f,
    BorderWidth            = 56f,     // rendered corner thickness on screen
    BorderImageRepeat      = BorderImageRepeat.Stretch,
    BorderImageFill        = BorderImageFill.Filled,
}

the two widths do different jobs. BorderImageWidth* is measured in the source texture's own pixels: it marks where the corner art ends, so it must be large enough to contain a corner. BorderWidth is the thickness the corner draws at on screen, so make it smaller than the slice and the corners scale down while the straight edges stretch to fill the gap. BorderImageRepeat controls how those edges fill (Stretch scales them, the tiling modes repeat them), and BorderImageFill.Filled keeps the center of the bitmap drawn behind the box rather than left transparent.

open a project in s&box and set BorderImageSource on a Container to try it yourself: drag the corner handle to resize the box and watch the corner art hold while the rails and center stretch.

engine-special blobs

SvgPanel, ScenePanel, WebPanel, Image, Sector, Arc, and Polygon wrap engine primitives that aren't general containers. all seven expose a common subset: sizing, margins, flex, Position and its four offsets (Top, Left, Right, Bottom), PointerEvents, BackgroundColor, BackgroundTint, Opacity, and Transform.

Image, SvgPanel, ScenePanel, and WebPanel additionally expose BorderRadius and its four corner variants. Image goes further: it's the only one of the seven with full border color and border width properties (BorderColor, BorderWidth, and the four per-edge variants of each), plus ObjectFit and ImageRendering. Sector, Arc, and Polygon instead pick up HoverBackgroundColor and TransitionMs, the state-variant pair (see shapes).

container-only styles include Padding, ZIndex, Gap, background-image, mask, filter, perspective, outline, Cursor, Order, and Overflow. to use one of those on a blob that doesn't expose it, wrap the blob in a Container:

new Container
{
    Position = PositionMode.Absolute,
    Left = 20,
    Top = 30,
    ZIndex = 1,
    Children = { new SvgPanel { Path = "ui/icon.svg" } },
}

SvgPanel already exposes Position, Left, and Top itself; the wrap above is only load-bearing here for ZIndex, a container-only property. reach for the same wrap whenever you need a container-only property alongside an engine-special blob, for example Padding and Gap around an Image, or a border color on Sector. Text doesn't need this treatment because it carries its own text styling subset on top of the common subset.

the manifest at tools/StyleFacadeEmit/style-manifest.json is the source of truth. each property lists which subsets it belongs to, each blob lists which subsets it subscribes to, and the emit tool generates Blob.Style.g.cs from those two lists.

pointer events

you rarely need to declare PointerEvents. goo auto-gates it: panels with event handlers (or TextEntry / WebPanel) get All, panels with state variants get All forced by the state controller (an explicit None is overridden there), and everything else gets None so inert decoration never eats clicks. the one common explicit declaration is PointerEvents.All on a scroll viewport, which the policy cannot detect. full rules and the state-variant caveat: events.


reference

every property each blob exposes, grouped by family:

see also