Styling & Theming
Styling in nano-ui is scoped: a Theme holds a Style per surface kind, and styled/themed modify that theme for a subtree. There is no per-widget CSS and no global mutable stylesheet.
For a live tour of the modifiers, see the Interactive Styling Lab. For non-obvious behavior, see Styling Gotchas.
The theme’s five surfaces
| Field | Used by |
|---|---|
themeButton | Buttons, toggles, dropdown triggers |
themeInput | Text fields, search boxes, numeric inputs, sliders |
themePanel | Cards, containers, sidebars, toolbars |
themePopup | Dropdowns, context menus, select popouts |
themeFloatingWindow | Modals, floating windows, inspectors |
A Style sets bg, fg, border, borderWidth, cornerRadius, hoverBg, and activeBg. A Theme also defines semantic colors: themeAccent / themeOnAccent, themeMuted, themeSelection, themeFocusRing, themeWindow, themeSeparator, and the success/warning/danger tones.
The page behind the view is pageBackground: themeWindow, unless the theme sets themePage. pageColor sets one. panelPage puts a view that draws no panel of its own on the panel surface. The page sits over the window background, which shows in the gaps between panels:
-- An app with no panel of its own: the panel surface is the page.
panelPageOptions :: SdlOptions
panelPageOptions = defaultSdlOptions {sdlAppTheme = Just (panelPage defaultTheme)}
-- Or scope it to a page with no panels.
body :: NanoUI ()
body = styled panelPage (columnWith (padAll 12) (label "Body"))
A full-window scroller (scrollWith grow) fills the page colour.
Built-in themes: defaultTheme (dark), defaultLightTheme, the Tomorrow variants, and themeFromBase16 / themeFromBase16Dark / themeFromBase16Light. Set one at startup with sdlAppTheme = Just appTheme, switch per frame with setUiTheme, or follow the OS with followSystemTheme / lightDark.
Scoped styling
themedViews = do
-- Replace the base theme for a subtree.
themed defaultLightTheme mainContent
-- Modify the active theme for a subtree.
whenM (styled (primary . rounded 999) (button "Continue")) continue
Modifiers compose with (.); nested scopes inherit the outer modification. Theme scopes affect painting only, never layout metrics.
background,foreground,borderColor,borderWidth,borderSides,cornerRadius,hoverBackground,pressBackground, andfillColorsetStylefields.buttonStyle,inputStyle,panelStyle,everyStyle,accentColor,textColor,rounded,subtle,tinted, andreadableOnmodify theTheme.primary,destructive,success,warning,tone Danger, andtoneColor Warningapply semantic colours.
A quiet button:
quiet :: Theme -> Theme
quiet t = (subtle . buttonStyle (foreground (themeMuted t))) t
quietSkip = whenM (styled quiet (button "Skip")) skip
For a fully custom look, define a palette record, build a Theme from it, and derive scoped helpers. Keep three surface levels (well, shelf, controls) and reserve color for meaning.
Interaction states
A widget is drawn in one of four states:
| State | Drawn with |
|---|---|
| Default | styleBg, styleFg, styleBorder |
| Hover | styleHoverBg |
| Pressed | styleActiveBg, while the primary button is held inside the widget |
| Disabled | Dimmed colours; pointer and keyboard input are ignored |
disabledWhen cond subtree disables interaction and dims rendering while preserving layout and state:
savingButtons = do
isSaving <- useVar False
saving <- readVar isSaving
disabledWhen saving $ row $ do
whenM (button "Cancel") cancel
whenM (button "Confirm") confirm
Shortcuts and pointer events inside a disabled subtree are suppressed automatically.
All four states, light and dark:
Typography and fonts
Font modifiers apply to any text-capable widget (labelWith, headingWith, textInputWith, buttonWith, …):
fontSize px,fontMono,fontBold(fontWeight),fontItalic,fontUnderline,fontStrike.fontTone Warning,fontColor color.
warningLabel = labelWith (fontMono . fontSize 15 . fontTone Warning) "warning: low disk"
The SDL backend bundles Inter and finds installed fonts by family name:
fontOptions :: SdlOptions
fontOptions =
defaultSdlOptions
{ sdlAppFont = FontSearch ["Segoe UI", "SF Pro Text", "Helvetica Neue"]
, sdlAppMonoFont = FontSearch ["Cascadia Mono", "JetBrains Mono", "Consolas"]
, sdlAppFontSize = 14
}
setSdlUiFont changes fonts mid-session. sdlAppUiScale = 0 follows the desktop scale on Windows. Measure text with lineWidth, fmLineHeight, and fmSnapScale, not by summing per-glyph advances.
The font modifiers side by side: