October 22nd, 2025Dev updates
It's been very important to us to make sure that PanGui has a highly capable styling system that can stand toe-to-toe with solutions like CSS.
Styling should easily support things like themes and skins, and it should do a good job of representing context-aware complexity and simplifying the IMGUI side of the code.
Styling should let you detach logic from visuals; every new unique Button implementation should not need different code. You shouldn't be forced to make a ToolbarButton(), TabButton(), LargeButton(), MiniButton(), etc - instead, you should be able to have a single Button() method that can have different visuals and layouting specified by styling.
And last, but not least, styling should solve the problem of default controls. Most IMGUI libraries have a distinctive default visual appearance that's difficult to get away from, because all of the basic controls are implemented using hard-coded visuals that cannot be overridden or extended without essentially reimplementing the entire library of basic controls. We wanted to avoid that kind of situation, which is why - until recently - PanGui had no default controls to speak of.
All of PanGui's built-in controls are now built using the styling system. PanGui will ship with default styles - which may be platform specific, if desired - which implement default visuals for tags like "button" and "toggle". As a result, all built-in controls are completely configurable and extendable by any user. The look of an entire application built using the default controls can easily be changed, simply by loading in another stylesheet.
As you begin reading through the very, very long list of features, you might worry about the performance implications of running such a "complicated" styling system in IMGUI, so we wanted to address that up front. As we created this system, it was extremely important to us that it be fast enough to realistically be used throughout an entire application. It took a lot of iteration, but we believe we've achieved both an incredible feature set, and extremely good IMGUI performance.
It is, of course, unrealistic to make it quite as fast as hard-coded IMGUI code, but we've strived to make the overhead as small as possible. Currently, the styling system is about ~3-5x slower than hard-coding the same visuals and layout. We think we could improve this a little bit more, but for now we are quite pleased with the performance numbers we're getting out of it.
This is also why PanGui's foundational performance and simple architecture is so important. We can afford to offer abstractions like the styling system that add some overhead, because PanGui's core is fast enough that these costs become almost negligible in practice. For a thousand UI elements, whether they are built with styling or not is typically only the difference between the GUI loop taking 0.05 or 0.15 ms.
Finally, we want to emphasize that making use of the styling system is not obligatory to have a "good" development experience with PanGui - making the core IMGUI API excellent is just as important to us. A fundamental part of the design philosophy of PanGui is that it has no preference or bias towards any particular level of abstraction. From working with raw mesh data buffers, to using the IMGUI "node builder" pattern, to using the styling system, to using retained mode patterns - every level of abstraction must be well designed, pleasant to engage with, and blend seamlessly with other levels of abstraction. The user can always choose, for any particular problem, what sort of level they prefer to work at.
But enough talk - let's get to the styling system. We're going to cover the following main points in this post:
- Loading style sheets - Load from strings, resources, or functions.
- Selectors - CSS-like selectors with modifiers, nesting and direct child combinator support.
- Inheritance - #inherit, #inherit-properties, #inherit-selector offer inheritance-like composition capabilities.
- Modifiers - Built-in modifiers (:hover, :hold, :focus) and custom modifiers with per-modifier transitions.
- Variables - Built-in variables like $width, $time and custom variables passed down from immediate mode, animatable through modifiers.
- Shapes - Full SDF shape API with combinations, transforms, and math expressions. Styles define, draw and animate multiple shapes.
- Effects - Support for stacking multiple effects (shadows, gradients, strokes) in order. Overriding and modifying effects is also possible.
- Macros - Pure text-substitution macros with parameter support: @const, @mixin, @style, @shape, @effect
- And much more - There are genuinely too many features and details to cover in a single blog post. We will continue to do more sneak peeks and feature highlights in later development updates, and a full overview will of course be available in the final documentation.
Let's start by getting the gist of the styling system, and then afterwards, we'll get into specific features. Here is a basic toggle style, as well as the IMGUI code that uses it.
1static bool Toggle(this Gui gui, bool state)
2{
3 LayoutNode node = gui.StyledNode("toggle", [state ? "enabled" : "disabled"]);
4
5 if (node.OnClick())
6 {
7 state = !state;
8 }
9
10 return state;
11}
1static bool Toggle(this Gui gui, bool state)
2{
3 LayoutNode node = gui.StyledNode("toggle", [state ? "enabled" : "disabled"]);
4
5 if (node.OnClick())
6 {
7 state = !state;
8 }
9
10 return state;
11}
1toggle {
2 height = 60;
3 width = ratio(2);
4
5 $tHandleOffset = 0;
6
7 :enabled(0.15 ease-in-out-sine) {
8 $tHandleOffset = 1;
9 }
10
11 bg-shape = roundedRect($width, $height, $height * 0.5);
12
13 effect bg-shape {
14 solidColor(rgba(0.3, 0.3 + $tHandleOffset / 0.7, 0.3, 1));
15 }
16
17 shape handleShape = moveX(circle($height * 0.4), ($width - $height) * ($tHandleOffset - 0.5));
18
19 :hover(0.1 ease-in-out-sine) {
20 shape handleShape = handleShape + 10;
21 }
22
23 :hold(0.05) {
24 shape handleShape = handleShape + 15;
25 }
26
27 effect handleShape {
28 solidColor(#f3f5f7);
29 outerShadow(#00000022, 5, 0);
30 outerShadow(#00000044, 15, 0, (0,5));
31 }
32}
As you can see, the logic and visuals of the toggle control are completely separate, here; the full appearance and visual behaviour of the toggle is defined through a stylesheet, including animations.
There are a few patterns here that are becoming common for us as we begin to use the styling system.
For example, toggle defines a variable ($tHandleOffset) which is used to control the alpha of the bg-shape's solidColor effect, and the positioning of the handle shape. This variable is then animated using the :enabled modifier, causing the shape and effect to also animate.
An alternate pattern is also on display here, where instead of defining variables that you use to animate shapes, you simply define the target shape directly in the modifier, as is done with :hover here, where the handle shape is expanded by 20px. Any arbitrary shape can mutate into any other arbitrary shape. This approach also works on effects.
Loading style sheets
It's quite simple to load stylesheets. You can provide the style text directly in the code, you can provide a function that provides the text whenever the stylesheet reloads, or you can provide a resource identifier - such as a file-path - which in that case will be loaded by the platform layer.
1Span<TextSource> styleSheetSources = [
2 TextSource.FromString("body { bg-color = black; }"),
3 TextSource.FromResourceIdentifier("style.pss"),
4 TextSource.FromStringFunction(SomeFunctionPointer),
5];
6
7StyleSheet styleSheet = StyleSheet.Create(styleSheetSources)
8
9Gui gui = new Gui(styleSheet);
During IMGUI, you can also swap out stylesheets if need be.
1gui.SetStyleSheet(newStyleSheet);
It is also possible to change or add to stylesheets after they've been created. In fact, many of the default platform layer implementations of PanGui provide hot reloading of stylesheet files during development, such that you see the result live in the application as you are editing the stylesheet.
Selectors
Styling supports CSS-like selectors with nesting and direct child combinators.
1
2dialog button {
3 min-width = 80;
4}
5
6
7sidebar > button {
8 padding = 15;
9 bg-color = #2a2a2a;
10}
11
12
13toolbar {
14 flow-dir = x;
15 gap = 10;
16 width = expand;
17
18 button {
19 padding = 10;
20 height = expand;
21
22 :hover(0.2 ease-out) {
23 bg-color = #3a3a3a;
24 }
25 }
26
27 > separator {
28 width = 2;
29 bg-color = #555555;
30 }
31}
In C#, the styling "tag hierarchy" used by selectors is then derived from the entering and exiting of style nodes:
1using (gui.EnterStyledNode("toolbar"))
2{
3
4
5 if (gui.Button("My Btn")) { }
6}
7
8
9EnteredStyledNode node = gui.EnterStyledNode("toolbar");
10
11node.Exit();
12
13
14
15static TagId ToolbarTag = TagId.FromString("toolbar");
16
17using (gui.EnterStyledNode(ToolbarTag)) { }
Inheritance
Unlike HTML where you can apply multiple classes (for example, <div class="box info-box">), PanGui gives each element only a single identifier (what we call a "tag"). Using inheritance, you can declare the same kind of relationship once in the stylesheet instead of repeating it at every call site:
1box {
2 padding = 20;
3 bg-color = white;
4}
5
6warning-box #inherit(box) {
7 bg-color = orange;
8}
9
10error-box #inherit(box) {
11 bg-color = red;
12}
13
14
15
16box > button
17{
18 padding = 10;
19}
1using (gui.EnterStyledNode("warning-box")) { }
When you use #inherit, two things happen:
- Selector matching - The style pretends to be its parent for selector purposes. That is, warning-box matches selectors targeting box.
- Property copying - All properties from the parent are copied into the child style.
Sometimes you only want one of these behaviors:
1
2info-panel #inherit-properties(box) {
3
4}
5
6
7info-panel #inherit-selector(box) {
8
9
10
11
12}
You can also inherit, copy from or pretend to be multiple styles:
1interactive-card #inherit(card, hoverable, clickable) {
2
3
4
5
6}
Modifiers
Built-in modifiers (:hover, :hold, :focus) respond to user interaction automatically. Custom modifiers let you animate any app-specific state. Each modifier defines its own transition duration and easing.
1checkbox {
2 width = 24;
3 height = 24;
4 bg-shape = roundedRect($width, $height, 4);
5
6 effect bg-shape {
7 solidColor(#e0e0e0);
8 stroke(#999, 1, 0);
9 }
10
11
12 :checked(0.2 ease-out) {
13 effect bg-shape {
14 solidColor(#4a90e2);
15 stroke(#4a90e2, 1, 0);
16 }
17 }
18
19
20 :hover(0.15) {
21 effect bg-shape {
22 stroke(#666, 1, 0);
23 }
24 }
25
26
27
28 :hover(0.25) {
29 bg-shape = bg-shape + 10;
30 }
31}
32
33
34
35button {
36 bg-shape = roundedRect($width, $height, 8);
37 $shadowSize = 4;
38
39 effect bg-shape {
40 solidColor(#4a90e2);
41 outerShadow(#00000033, $shadowSize, 0, (0, 2));
42 }
43
44 :hover(0.15 ease-out) {
45 $shadowSize = 8;
46 }
47
48 :hold(0.1 ease-in) {
49 $shadowSize = 2;
50 scale = 0.95;
51 }
52}
Using modifiers in C# is also quite simple. Exactly like styles, modifiers are also simply tags:
1static bool Checkbox(this Gui gui, bool isChecked)
2{
3 LayoutNode node = gui.StyledNode("checkbox", [isChecked ? "checked" : null]);
4
5 if (node.OnClick())
6 isChecked = !isChecked;
7
8 return isChecked;
9}
Variables
It is possible to define variables, which can be mutated by modifiers, and which can be used in shape and effect expressions. Variables can be declared inside styles, or in the root stylesheet scope itself, in which case they are global variables.
There are also several built-in variables which can always be used:
- $width: The width of the node
- $height: The height of the node
- $time: The current time in seconds
- $content-width: The width of the node's content
- $content-height: The height of the node's content
- $cursor-position: The screen position of cursor 0
We expect to add more built-in variables over time, as we discover the need for them.
It's also possible to override or provide variables from IMGUI code:
1float progress = 0.65f;
2
3Span<StyleVariable> vars = [
4 ("progress", progress)
5];
6
7
8gui.StyledNode("progress-bar", vars);
1progress-bar {
2 height = 30;
3 bg-shape = roundedRect($width, $height, 15);
4
5 effect bg-shape {
6 solidColor(#2a2a2a);
7 }
8
9
10 shape fill = roundedRect(
11 $width * $progress,
12 $height,
13 15
14 );
15
16 effect fill {
17 solidColor(#4a90e2);
18 }
19}
1float progress = 0.65f;
2
3Span<StyleVariable> vars = [
4 ("progress", progress)
5];
6
7
8gui.StyledNode("progress-bar", vars);
1progress-bar {
2 height = 30;
3 bg-shape = roundedRect($width, $height, 15);
4
5 effect bg-shape {
6 solidColor(#2a2a2a);
7 }
8
9
10 shape fill = roundedRect(
11 $width * $progress,
12 $height,
13 15
14 );
15
16 effect fill {
17 solidColor(#4a90e2);
18 }
19}
1loading-spinner
2{
3 width = 50;
4 height = 50;
5
6
7 $rotation = $time * 6.28;
8
9 shape spinner =
10 move(
11 circle(20),
12 (cos($rotation) * 15, sin($rotation) * 15)
13 );
14
15 effect spinner {
16 solidColor(white);
17 }
18}
Shapes
To express visuals more complicated than a rectangle, the styling gives you full access to the shapes API, letting you type out shape and math expressions. A style can have as many shapes as you'd like, and shapes can of course be changed by modifiers.
All styles have an implicit shape, bg-shape, which defaults to a simple rect the size of the node. The bg-shape is used to represent the interactive area of the styled node, and is also used to define the area that gets clipped when the clip-content property is true.
Shapes are by default always positioned at the center of the node, but can be moved using the move, move-x and move-y functions.
1nodeExample {
2
3 bg-shape = roundedRect($width, $height, 8);
4
5
6 clip-content = true;
7
8
9 shape myRect = roundedRect($width, $height, 8);
10
11
12 shape outer = circle(50);
13 shape inner = circle(35);
14 bg-shape = outer - inner;
15
16
17 shape icon = circle(15);
18
19 :hover(0.2) {
20 shape icon = icon + 5;
21 }
22
23 effect icon {
24 solidColor(white);
25 }
26}
You can also define reusable shape macros (see the Macros section below).
Available shape functions: circle, rect, roundedRect, arc, pie, triangle, union, intersect, subtract, inverse, expand, move, moveX, moveY, rotate, mix, onion.
Effects
Shapes, by themselves, do nothing. In order for a shape to be displayed, you must create effects, which are lists of graphical effects applied in order to a given shape. Order also matters - effects are applied top to bottom.
1elevated-card {
2 bg-shape = roundedRect($width, $height, 12);
3
4 effect bg-shape {
5 solidColor(white);
6 outerShadow(#00000022, 15, 0, (0, 4));
7 outerShadow(#00000011, 30, 0, (0, 8));
8 stroke(#e0e0e0, 1, 0);
9 }
10}
11
12gradient-button {
13 bg-shape = roundedRect($width, $height, 8);
14
15 effect bg-shape {
16 linearGradient(#667eea, #764ba2, 0, $height);
17 outerShadow(#764ba255, 10, 0, (0, 4));
18 }
19
20 :hover(0.3) {
21 effect bg-shape {
22 linearGradient(#667eea, #764ba2, 0, $height);
23 outerShadow(#764ba255, 15, 0, (0, 6));
24 }
25 }
26}
You can also define reusable effect macros (see the Macros section below).
The currently available effects are: solidColor, linearGradient, radialGradient, outerShadow, innerShadow and stroke. We expect to add more effects later, such as backdrop blur, glass effects and so on.
Macros
Macros are indicated with @, both at the point of declaration and the point of use. They provide pure text substitution during parsing. There are several distinct macro types: @const for constants, @mixin for reusable style content blocks, @style for generating styles, and @shape and @effect for reusable graphics.
Many macro types also support having parameters, which are simple text snippets that are provided at the point of usage, and substituted into the macro contents before it is parsed.
Macros also support recursive nesting; before a macro is expanded, it will expand any macros it itself contains.
Finally, we've gone to great lengths to be able to provide good and comprehensible error messages when things go wrong with macros, as opaque errors are a common pain point of macros in many languages.
Constants
Constants are, in effect, immutable constant values that are declared ahead of time. They can be used in any expression or property value.
1@const spacing = 16;
2@const primaryColor = #4a90e2;
3@const animSpeed = 0.3;
4
5box {
6 padding = @spacing;
7 border-radius = @spacing * 0.5;
8 bg-color = @primaryColor;
9}
Mixins
Mixins are reusable style content blocks. For example, this is a quick utility for visualizing the structure you're working with, which will draw a single pixel stroke around every styled node inside of a given style:
1@mixin showAllChildren() {
2 effect bg-shape {
3 solidColor(#ff00ff55);
4 stroke(#ffffff77, 1, 1);
5 }
6
7
8 * {
9 effect bg-shape {
10 stroke(#ffffff77, 1, 1);
11 }
12 }
13}
14
15
16my-container {
17 @showAllChildren();
18}
Mixins can also take parameters, which is very useful for creating reusable appearance "functions":
1@mixin card-appearance(@radius, @shadowSize) {
2 bg-shape = roundedRect($width, $height, @radius);
3
4 effect bg-shape {
5 solidColor(white);
6 outerShadow(#00000022, @shadowSize, 0);
7 }
8}
9
10card {
11 @card-appearance(12, 10);
12}
Style Macros
Style macros are macros for declaring styles. They're great for when you need lots of variations of the same kind of style, with only minor differences.
For example, they're perfect for generating icon styles from font characters:
1@style make_icon_style(@iconName, @unicodeChar) = @iconName {
2 text-font = "fa-regular-400.ttf";
3 text-content = "\u@unicodeChar";
4 text-size = 16;
5 text-align = (0.5, 0.5);
6}
7
8
9@make_icon_style(icon_save, f0c7);
10@make_icon_style(icon_open, f07c);
11@make_icon_style(icon_close, f00d);
12@make_icon_style(icon_menu, f0c9);
13@make_icon_style(icon_settings, f013);
Usage:
1gui.StyledNode("icon_save");
2gui.StyledNode("icon_settings");
Shape and Effect Macros
Sometimes, you want to use specific kinds of shapes or sets of effects in many different places, perhaps with minor variations. Shape and effect macros are perfect for this.
1
2@shape pill = roundedRect($width, $height, min($width, $height) * 0.5);
3
4
5@shape rounded-box(@radius) = roundedRect($width, $height, @radius);
6
7
8@effect material-shadow() {
9 outerShadow(#00000022, 8, 0, (0, 2));
10 outerShadow(#00000011, 16, 0, (0, 4));
11}
12
13@effect elevation(@level) {
14 outerShadow(#00000022, @level * 4, 0, (0, @level));
15 outerShadow(#00000011, @level * 8, 0, (0, @level * 2));
16}
17
18
19button {
20 bg-shape = @pill;
21
22 effect bg-shape {
23 solidColor(#4a90e2);
24 @material-shadow();
25 }
26}
27
28card-subtle {
29 bg-shape = @rounded-box(8);
30
31 effect bg-shape {
32 solidColor(white);
33 @elevation(1);
34 }
35}
36
37card-prominent {
38 bg-shape = @rounded-box(16);
39
40 effect bg-shape {
41 solidColor(white);
42 @elevation(4);
43 }
44}
And much more
We could never cover everything about PanGui's styling system in a single blog post - at least, not without making it several times longer than this one, which is already getting plenty long! But there are a few more minor points we'd like to briefly mention.
Error reporting
For any given language, the user interface is essentially the error reporting system for that language. Syntax errors should explain clearly what is wrong, where it is wrong, and provide suggestions on how to fix it, serving essentially as a teacher and helpful guide.
In addition to its many other features, PanGui's debugger lets you see styling errors as runtime overlays in your application, so you get instant feedback during development. This works especially well with hot reloading of styles.
Debugging
The debugger also has tabs that are very helpful with debugging the style system. For example, you can inspect the layout hierarchy, and see which style properties were applied to a given node, where they came from, and so on - very similar to the view you are presented with when you open the developer tools in a browser.
We will be showing these debugger features off in a future dev update.
All the properties we didn't cover...
- Any property can be animated/changed using modifiers.
- Animate from fit to 200px or any other size type seamlessly, with full wrap support.
- z-index for layering.
- position-type with values: part-of-layout, relative-to-root, relative-to-parent.
- wrap can be set to a number to specify item count per row/column, or to true or 0 for auto-wrapping, or false for no wrapping.
- flow-dir for layout direction, with values: x, y, and eventually x-reverse, y-reverse.
- Typography controls: text-font, text-size, text-color, text-align, text-offset, line-height, text-content including wrapping with word-wrap = no-wrap-and-no-break, no-wrap, word, letter.
- padding, margin, gap with per-side control.
- min-width, max-width, min-height, max-height.
- clip-content for overflow handling.
- Blend modes and color blending with blend-mode and blend-color.
- Multiple size modes: pixels, percentage, ratio, fit, expand.
- Transform properties: translate, scale, scale-pivot, rotation, rotation-pivot.
- Content align: content-align, content-align-x, content-align-y, content-offset, content-offset-x, content-offset-y alignment uses floats (0 to 1) instead of (begin, middle, end), making everything animatable. Centering something is never more complicated than content-align = 0.5.
- spacing for distributing items with values: none, space-around, space-between, space-evenly.
- And more to come!
That concludes it for today. We hope you've enjoyed this update on our styling system. It was longer than we intended to make it, really - but there was a lot of ground to cover! Styling is easily the most iterated-upon feature in all of PanGui to date, and it has gone through many versions and reworks before we arrived where we are today.
We're also, of course, interested in hearing what you think about it. If you have thoughts or questions, please join our Discord to join the discussion about PanGui.
All the best,
The PanGui Team