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.
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.
light.kss.
dark.kss.
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.
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 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);
}
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()
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, andSlider. - 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:pressedstate currently applies to button-like widgets.
The parser accepts CSS-like names plus a few Klyn-specific names. Some declarations only have an effect on widgets whose painter consumes them.
font, font-family, font-size, color,
text-color, placeholder-color, placeholder-text-color,
selection-color, and caret-color.
background, background-color, background-color-2,
background-gradient-to, border-color, border-width,
corner-radius, accent-color, and track-fill-color for
sliders.
margin, margin-left, margin-top, margin-right,
margin-bottom, padding, padding-x, padding-y,
padding-left, padding-top, padding-right, and
padding-bottom.
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.
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.
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;
}
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_SYSTEMfor normal applications; reserve forced light/dark mode for demos, previews, or explicit user preferences. - Prefer
accent-colorfor 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.
Continue with FormLayout to build label/field forms with the themed widgets.