Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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

FieldUsed by
themeButtonButtons, toggles, dropdown triggers
themeInputText fields, search boxes, numeric inputs, sliders
themePanelCards, containers, sidebars, toolbars
themePopupDropdowns, context menus, select popouts
themeFloatingWindowModals, 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, and fillColor set Style fields.
  • buttonStyle, inputStyle, panelStyle, everyStyle, accentColor, textColor, rounded, subtle, tinted, and readableOn modify the Theme.
  • primary, destructive, success, warning, tone Danger, and toneColor Warning apply 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:

StateDrawn with
DefaultstyleBg, styleFg, styleBorder
HoverstyleHoverBg
PressedstyleActiveBg, while the primary button is held inside the widget
DisabledDimmed 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: