Guide
Styles and themes #
All visual properties go through one type: ui.Style. A theme gives names
to colors, sizes, and radii. This page explains how Knots combines styles and resolves
them against a theme.
The Style type #
Every field of ui.Style is optional. A null field means "this
style does not set it". The fields are in five groups:
| Group | Fields |
|---|---|
| Layout |
width, height, padding, gap,
direction, @"align", justify,
overflow, position, offset,
layer, grid, grid_cell. Read
Layout engine.
|
| Surface |
background, border_width, border_color,
radius, state_layer, opacity,
backdrop
|
| Content |
foreground, font, font_size,
tone, wrap
|
| States |
hover, focus, active,
checked, open, disabled
|
| Motion | transition |
A component takes a pointer to a style. Zig lets you write it inline:
try frame.e(Rect{ .key = .src(@src()), .style = &.{
.padding = .all(16),
.background = .elevated,
.radius = .lg,
.border_width = .all(1),
.border_color = .toned,
} });
How Knots combines styles #
For each element, Knots makes one resolved style in this order:
- It starts with the base style of the component.
-
It applies your
style. Each field that you set replaces the base field. Fields that you do not set keep the base value. - It applies the state styles that are active, first from the base, then from your style.
-
It gets content fields that are still
nullfrom the parent element. - It resolves theme tokens to real values.
Thus you change only what you need. For example, the base style of
Button sets the padding, the colors, the radius, and the hover effect. This
style makes a button wider and keeps everything else:
Button{ .key = .src(@src()), .label = "Save", .style = &.{ .width = .fixed(120) } }
Inheritance #
foreground, font, font_size, and
tone are inherited. If an element does not set them, it uses the value of
its parent. Set font_size = .sm on a panel, and all text in the panel is
small. wrap is not inherited.
Reuse styles #
A style is a normal value. Declare shared styles as constants. Use
with to make a variant. with works at compile time.
const card: ui.Style = .{
.padding = .all(16),
.background = .elevated,
.radius = .lg,
};
const selected_card = card.with(.{ .border_width = .all(2), .border_color = .accent });
try frame.e(Rect{ .key = .src(@src()), .style = &comptime card.with(.{ .width = .grow() }) });
Use &comptime when you call with inline. Then the style
is a constant and the pointer stays valid.
State styles #
A state field holds a style that Knots applies when the state is active. Each component decides which states it has.
| State | Active when |
|---|---|
hover |
The pointer is over the element. |
focus |
The element has keyboard focus. |
active |
The user presses the element. |
checked |
A checkbox or a radio button is on. |
open |
A menu, select, or popup is open. |
disabled |
The component is disabled. |
This style is for the box of a radio button:
const option_box: ui.Style = .{
.background = .transparent,
.foreground = .dimmed,
.hover = &.{ .background = .muted },
.checked = &.{
.foreground = .text,
.hover = &.{ .background = .accented }, // checked AND hover
},
};
- Knots applies the states in this order: hover, focus, active, checked, open, disabled.
- A state inside a state applies only when both are active.
-
disabledwins. A disabled element also ignores hover, focus, and active.
Colors #
A color field takes a ui.Color.Input. Usually this is a theme token. Tokens
change when the theme changes.
| Tokens | Use |
|---|---|
.primary, .secondary, .success,
.info, .warning, .@"error"
|
Accent colors with a meaning. |
.on_primary … .on_error |
Text that is easy to read on the matching accent color. |
.bg, .muted, .elevated,
.accented, .inverted
|
Backgrounds. |
.text, .highlighted, .dimmed,
.toned
|
Text, strong text, weak text, and borders. |
.accent, .on_accent |
The color of the current tone and its text color. |
.current |
The resolved foreground of the element, like CSS currentColor. |
.transparent |
No color. |
.{ .color = c } |
A fixed ui.Color that does not follow the theme. |
Make a fixed color from sRGB values:
const brand = comptime ui.Color.hex("#ff7a18") catch unreachable;
const shadow = ui.Color.rgba(0, 0, 0, 64);
.background = .{ .color = brand },
hex accepts #RGB, #RGBA, #RRGGBB,
and #RRGGBBAA. Both functions convert sRGB to linear color, because Knots
blends in linear space.
Tone #
tone changes what .accent and .on_accent mean
for an element and its children. The values are .primary (default),
.secondary, .success, .info,
.warning, .@"error", and .neutral.
Button{ .key = .src(@src()), .label = "Delete", .style = &.{ .tone = .@"error" } }
The button base style uses .accent for the background and
.on_accent for the label. With the tone set, the button is red and the
hover effect and label color follow.
Surface effects #
-
state_layermixes the foreground color over the background by the given amount. For example,0.15gives a light hover effect on any background. -
opacitymultiplies all alpha values of the element: the background, the border, and the foreground. -
backdropblurs or bends what is behind the element. Useui.Material.frostedorui.Material.glass, or set the fieldsblur,saturation,refraction,bezel,dispersion, andspecular. Thebackgroundcolor tints the result.
.style = &.{
.background = .{ .color = ui.Color.rgba(255, 255, 255, 24) },
.backdrop = ui.Material.glass,
.radius = .xl,
},
Size tokens #
Radius and font size also accept theme tokens:
| Field | Tokens | Other values |
|---|---|---|
radius |
.xs (0.25×), .sm (0.5×), .md (1×),
.lg (1.5×), .xl (2×) of the theme radius
|
.none, .{ .fixed = px },
.{ .corners = .{ tl, tr, br, bl } }
|
font_size |
.xs, .sm, .md, .lg,
.xl. Default sizes: 12, 16, 20, 24, 28.
|
.{ .px = size } |
border_width takes .all(px) or
.edges(top, right, bottom, left).
Transitions #
Set transition to animate changes between states. Knots animates the
surface (background, border, radius, backdrop) and the foreground. It does not animate
layout fields.
.transition = .{ .duration_ms = 150, .ease = .ease_out_cubic },
The default is 120 ms with .smooth_step.
Parts #
A component with more than one element has parts. Each part is a style for
one inner element. Parts use the same rules as style.
SliderInput{
.key = .src(@src()),
.value = &volume,
.style = &.{ .width = .fixed(200), .tone = .success },
.parts = .{
.track = &.{ .height = .fixed(6) },
.thumb = &.{ .width = .fixed(18) },
},
}
The component catalog lists the parts of each component.
Themes #
A ui.Theme holds the values of all tokens. Knots has
ui.Theme.light (the default) and ui.Theme.dark. Select one
when you create the app:
var app = try knots.App.init(io, gpa, .{
.window = .{ .width = 800, .height = 600, .title = "App" },
.ui = .{ .theme = ui.Theme.dark },
});
To change the theme while the app runs, set it at the start of a frame:
frame.ui().theme = if (dark_mode) ui.Theme.dark else ui.Theme.light;
Make a theme #
Write the theme as a ZON file, and parse it at compile time with
ui.Theme.parse. Set every field. A field that you do not set is zero,
which is transparent black for a color.
// themes/ocean.zon
.{
.primary = .{ .hex = "#3b82f6" },
.secondary = .{ .hex = "#8b5cf6" },
.success = .{ .hex = "#22c55e" },
.info = .{ .hex = "#0ea5e9" },
.warning = .{ .hex = "#f59e0b" },
.@"error" = .{ .hex = "#ef4444" },
.on_primary = .{ .hex = "#ffffff" },
.on_secondary = .{ .hex = "#ffffff" },
.on_success = .{ .hex = "#ffffff" },
.on_info = .{ .hex = "#ffffff" },
.on_warning = .{ .hex = "#1e1e21" },
.on_error = .{ .hex = "#ffffff" },
.bg = .{ .hex = "#0b1220" },
.elevated = .{ .hex = "#111a2e" },
.muted = .{ .hex = "#1a2540" },
.accented = .{ .hex = "#e2e8f0" },
.inverted = .{ .hex = "#0b1220" },
.text = .{ .hex = "#e2e8f0" },
.highlighted = .{ .hex = "#ffffff" },
.toned = .{ .hex = "#334155" },
.dimmed = .{ .hex = "#94a3b8" },
.radius = 6,
.font_size = .{ 12, 16, 20, 24, 28 },
.scrollbar_thickness = 8,
.scrollbar_min_thumb = 24,
.scrollbar_track_color = .{ .hex = "#1a254080" },
.scrollbar_thumb_color = .{ .hex = "#475569" },
.scrollbar_thumb_hover_color = .{ .hex = "#e2e8f0" },
.scrollbar_corner_radius = 4,
}
const ocean = ui.Theme.parse(@import("themes/ocean.zon"));
To change only some fields of an existing theme, use
ui.Theme.parseWithBase(ui.Theme.dark, .{ .primary = .{ .hex = "#ff7a18" } }).
Style contract for component authors #
All built-in components follow one contract. A test in Knots checks it. Follow it in your own components so that they work like the built-in ones:
- A
style: *const ui.Style = &.{}field. - A
pub const basenamespace with aroot: ui.Style. -
For more than one element: a
Partsstruct of*const ui.Stylefields, and onebasedeclaration for each part. -
No other fields for colors, sizes, padding, alignment, or layers. These go in
styleorparts.