GUI KSS Themes

GUI Themes and KSS

Klyn GUI widgets are styled through KSS, a small CSS-like language used by klyn.gui.windows. The built-in light and dark themes define the default desktop look, while applications can force an appearance mode or install their own KSS stylesheet at startup.

Built-In Light and Dark Themes

The standard themes are stored under KLYN_HOME/lib/klyn/gui/windows/themes as light.kss and dark.kss. By default, windows use ThemeManager.MODE_SYSTEM, which maps to the operating-system appearance when Klyn can detect it. The screenshots below are generated from samples/gui/Calculator.kn with the proposed built-in KSS themes.

Klyn calculator rendered with the light GUI theme
Calculator sample rendered with light.kss.
Klyn calculator rendered with the dark GUI theme
Calculator sample rendered with dark.kss.
Controlling Appearance Mode

Use ThemeManager.setAppearanceMode() when the whole application should follow a mode. Use window.appearanceMode when one window must override the global mode. Set the mode before creating and laying out the window when possible, because font and padding declarations can affect preferred sizes.

import klyn.gui.windows
import klyn.gui.windows.themes

# Global choice for windows that keep MODE_SYSTEM.
ThemeManager.setAppearanceMode(ThemeManager.MODE_DARK)

win = MainWindow("Themed window")
win.appearanceMode = ThemeManager.MODE_SYSTEM
win.size = (480, 320)
win.centerIn(null)
win.show()
win.run()

Valid modes are ThemeManager.MODE_SYSTEM, ThemeManager.MODE_LIGHT, and ThemeManager.MODE_DARK. A registered custom theme can also be selected by its name, for example "harbor". On platforms where system detection is not available, system falls back to the light theme unless an environment hint is present.

Defining a Custom KSS Stylesheet

A custom stylesheet is an ordinary .kss file following the same syntax as the built-in themes. Load it at startup with ThemeManager.setThemeFromFile() and give it an application-specific name. This does not replace light.kss or dark.kss.

import klyn.gui.windows
import klyn.gui.windows.themes
import klyn.io

ThemeManager.setThemeFromFile(
    "harbor",
    FilePath(Path.klynHome, "themes", "harbor.kss")
)
ThemeManager.setAppearanceMode("harbor")

Keep application themes separate from the standard library themes. The files under KLYN_HOME/lib/klyn/gui/windows/themes are the default GUI contract of the runtime; an application should ship its own KSS files next to its sources or in a dedicated resource directory.

KSS Syntax

KSS keeps CSS block syntax, selector names, and declarations, but deliberately supports only the subset consumed by Klyn widgets. Shared design tokens can be declared in :root as CSS-style variables and reused with var(--name) or var(--name, fallback). This makes parsing cheap while keeping styles maintainable.

:root {
    --font-ui: Segoe UI Variable Text, Segoe UI, DejaVu Sans, Arial, sans-serif;
    --font-size-base: 16px;
    --surface: #173B3F;
    --surface-strong: #0F2A2E;
    --surface-input: #FFF7E8;
    --text: #F8ECD7;
    --text-muted: #A8DADC;
    --text-on-input: #123235;
    --text-on-accent: #102A2E;
    --accent: #F4A261;
    --accent-alt: #2A9D8F;
    --accent-hover: #E9C46A;
    --border-soft: #40676B;
    --radius-card: 16px;
    --radius-control: 10px;
    --radius-button: 12px;
    --window-padding: 10px;
    --card-padding: 18px;
    --field-padding: 10px 14px;
    --button-padding: 11px 20px;
    --button-margin: 6px 0px;
}

Widget {
    font-family: var(--font-ui);
    font-size: var(--font-size-base);
    color: var(--text);
    accent-color: var(--accent);
}

MainWindow {
    background: var(--surface-strong);
    padding: var(--window-padding);
}

Container.preview-card {
    background: var(--surface);
    border-color: var(--accent-alt);
    border-width: 1px;
    corner-radius: var(--radius-card);
    padding: var(--card-padding);
}

Label.title {
    font-size: 24px;
    color: var(--text);
}

Label.muted {
    color: var(--text-muted);
}

TextBox {
    background: var(--surface-input);
    color: var(--text-on-input);
    border-color: var(--accent-hover);
    border-width: 2px;
    corner-radius: var(--radius-control);
    padding: var(--field-padding);
    selection-color: var(--accent);
    caret-color: var(--accent);
}

Button {
    background: var(--accent);
    color: var(--text-on-accent);
    border-color: var(--text);
    border-width: 0px;
    corner-radius: var(--radius-button);
    padding: var(--button-padding);
    margin: var(--button-margin);
}

Button.secondary {
    background: var(--accent-alt);
    color: var(--text);
}

Button:hover {
    background: var(--accent-hover);
}

Slider {
    color: var(--text);
    border-color: var(--border-soft);
    accent-color: var(--accent);
    track-fill-color: var(--accent-alt);
}
Klyn GUI rendered with a custom teal and amber KSS theme
Custom KSS preview rendered with the stylesheet above. It deliberately uses a teal and amber visual direction instead of extending the built-in light or dark themes.

Save the stylesheet above as themes/harbor.kss, then load it before constructing the widgets. The preview window can be written as follows:

import klyn.gui.windows
import klyn.gui.windows.layouts
import klyn.gui.windows.themes
import klyn.io

ThemeManager.setThemeFromFile(
    "harbor",
    FilePath(Path.klynHome, "themes", "harbor.kss")
)
ThemeManager.setAppearanceMode("harbor")

class HarborThemePreview extends MainWindow:

    public HarborThemePreview():
        super("Custom KSS Preview")
        this.size = (560, 390)
        this.centerIn(null)

        root as Container = this.centralWidget
        rootLayout as VBoxLayout = VBoxLayout(spacing=14)
        root.layout = rootLayout

        cardLayout as VBoxLayout = VBoxLayout(spacing=12)
        card as Container = Container(cardLayout)
        card.styleClass = "preview-card"
        card.layoutParams = LayoutParams(1, 1)

        title as Label = Label("Harbor Control")
        title.styleClass = "title"
        cardLayout.add(title)

        subtitle as Label = Label("Teal surfaces, warm amber actions.")
        subtitle.styleClass = "muted"
        cardLayout.add(subtitle)

        field as TextBox = TextBox("captain@klyn.local")
        field.preferredHeight = 48
        cardLayout.add(field)

        slider as Slider = Slider(value=64, minimum=0, maximum=100)
        slider.showTicks = true
        slider.showValues = true
        slider.tickInterval = 25
        slider.preferredHeight = 86
        cardLayout.add(slider)

        rowLayout as HBoxLayout = HBoxLayout(spacing=12)
        row as Container = Container(rowLayout)
        row.preferredHeight = 54

        details as Button = Button("Details")
        details.styleClass = "secondary"
        rowLayout.add(details)

        launch as Button = Button("Launch")
        rowLayout.add(launch)

        cardLayout.add(row)
        rootLayout.add(card)

window = HarborThemePreview()
window.run()
Selectors

KSS selectors are resolved from general to specific. Later matching rules overlay earlier rules, so Button.primary can refine the base Button style.

  • Widget selectors: Widget, Window, MainWindow, Container, Canvas, Label, AbstractButton, Button, CheckBox, TextBox, ComboBox, and Slider.
  • Style classes: set widget.styleClass = "danger compact", then target .danger, Button.danger, or another widget-qualified class selector.
  • Pseudo states: :hover, :focus, :pressed, and :disabled. The :pressed state currently applies to button-like widgets.
Supported Declarations

The parser accepts CSS-like names plus a few Klyn-specific names. Some declarations only have an effect on widgets whose painter consumes them.

Text and color

font, font-family, font-size, color, text-color, placeholder-color, placeholder-text-color, selection-color, and caret-color.

Surfaces, borders, and accents

background, background-color, background-color-2, background-gradient-to, border-color, border-width, corner-radius, accent-color, and track-fill-color for sliders.

Layout metrics

margin, margin-left, margin-top, margin-right, margin-bottom, padding, padding-x, padding-y, padding-left, padding-top, padding-right, and padding-bottom.

Shadow fields

shadow-color, shadow-blur, shadow-offset-x, and shadow-offset-y are parsed into ThemeStyle. A widget only shows them when its painter explicitly implements shadow rendering.

Variables

Declare design tokens in :root with names such as --accent or --radius-control. Any declaration value can then use var(--accent). Use var(--missing, fallback) when a variable is optional.

Using Style Classes

Use styleClass when a widget has a semantic role that should not require a new widget subclass. This keeps style decisions in KSS while preserving statically typed widget code.

deleteButton = Button("Delete")
deleteButton.styleClass = "danger"

primaryButton = Button("Save")
primaryButton.styleClass = "primary"
Button.danger {
    background: #B42318;
    color: white;
}

Button.primary:focus {
    border-color: #7CC4FF;
}
Practical Rules
Keep themes stable

Put default GUI language styling in the built-in light and dark themes. Put application identity, brand colors, and one-off visual decisions in an application KSS file or in the sample itself. This separation avoids changing every application when the standard widget theme evolves.

  • Set global or custom themes before creating windows.
  • Use MODE_SYSTEM for normal applications; reserve forced light/dark mode for demos, previews, or explicit user preferences.
  • Prefer accent-color for interactive highlights so buttons, sliders, focus indicators, and selection colors stay coherent.
  • Do not rely on unsupported CSS features such as cascading combinators, media queries, percentages, or arbitrary units. KSS intentionally stays small.
Next Step

Continue with FormLayout to build label/field forms with the themed widgets.