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:
-
shorthand vs per-edge - when both are set, the per-edge value wins.
new Container { Padding = 16, PaddingTop = 32 } // 32 top, 16 elsewherethe same rule applies to
Margin,BorderColor,BorderWidth,BorderRadius, andGap. -
FlexDirection - defaults to
Row, and it's the only non-nullable property. -
font inheritance -
FontFamily,FontSize, and the rest of the font properties set on aContainerapply to descendantTextblobs through the engine.
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:
- container - 150 style properties (the full style surface).
- text - 52 style properties (a subset of container, including BackgroundTint).
- image - 41 style properties (the largest of the engine-special blobs, thanks to its border color and width properties).
- sector - sector arc blob style reference.
- arc - arc blob style reference.
- polygon - polygon blob style reference.
- svgpanel - svg panel blob style reference.
- scenepanel - scene panel blob style reference.
- webpanel - web panel blob style reference.
- textentry - text entry blob style reference.
see also¶
- overview - the model, blobs, and the tree
- tokens - design tokens and theming patterns
- animations - animating style properties
- events - the PointerEvents auto-gate in full, including the state-variant caveat
- shapes -
Sector,Arc, andPolygonguidance, including the state-variant pair - svg panel -
SvgPanelguidance, sizing, tinting, and the wrapper pattern - web panel -
WebPanelguidance for embedding a live webview - container - the full style surface reference
- text - text blob style reference
- image - image blob style reference