Skip to content

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:

  1. It starts with the base style of the component.
  2. It applies your style. Each field that you set replaces the base field. Fields that you do not set keep the base value.
  3. It applies the state styles that are active, first from the base, then from your style.
  4. It gets content fields that are still null from the parent element.
  5. 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.
  • disabled wins. 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_layer mixes the foreground color over the background by the given amount. For example, 0.15 gives a light hover effect on any background.
  • opacity multiplies all alpha values of the element: the background, the border, and the foreground.
  • backdrop blurs or bends what is behind the element. Use ui.Material.frosted or ui.Material.glass, or set the fields blur, saturation, refraction, bezel, dispersion, and specular. The background color 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 base namespace with a root: ui.Style.
  • For more than one element: a Parts struct of *const ui.Style fields, and one base declaration for each part.
  • No other fields for colors, sizes, padding, alignment, or layers. These go in style or parts.