Styling

In Day the platform draws the widgets: a Day button on macOS is an NSButton with AppKit’s chrome; on Android it’s a Material button. So styling works differently than in a renderer, where you control every pixel.

You style content and space; the platform styles controls. Fonts, text color, padding, backgrounds, corner radii, and everything you draw in a canvas are yours. Button chrome, focus rings, slider tracks, scrollbar appearance, selection highlights: those belong to the platform, and Day leaves them there rather than reimplementing them.

Text

Fonts are semantic-first. Instead of hardcoding point sizes, pick a role and let each platform map it to its own text-style system:

label(tr("title")).font(Font::Title)
label(tr("caption")).font(Font::Caption).color(Color::rgb(0.5, 0.5, 0.5))
label("Total").font(Font::Body).weight(FontWeight::Semibold)
label("legalese").italic()

The semantic roles (Title, Title2, Title3, Headline, Subheadline, Body, Callout, Footnote, Caption, Caption2) resolve to the platform’s typography scale, which is what keeps text looking correct next to native controls. Font::System(18.0) is the escape hatch when you need an exact size, and Font::Custom("Family", 18.0) renders a font you bundle in the project’s resource/fonts/ directory (resources guide).

Color, backgrounds, shape

column((avatar, name, bio))
    .padding(16.0)
    .background(Color::hex(0x1E293B))
    .corner_radius(12.0)

Color is a plain sRGB value (Color::rgb, Color::rgba, Color::hex(0xRRGGBB), plus BLACK, WHITE, CLEAR). .background() accepts a static color or a reactive one (a closure or signal), so appearance can follow state:

label(move || status.get().to_string())
    .background(move || if error.get() { RED_TINT } else { Color::CLEAR })

A limitation to plan around: Day does not yet ship semantic color tokens or automatic light/dark adaptation for the colors you specify. Native widget chrome follows the system appearance on its own (an NSButton is correct in dark mode without your help), but a hardcoded Color::hex(0xFFFFFF) background is white in both modes. The design reserves a token system (theme::TEXT, theme::CARD, … resolving to UIColor.label, Adwaita named colors, and so on) that hasn’t landed yet; until it does, apps that want dark-mode-aware custom colors carry their own palette and switch it themselves. If you can avoid custom colors on large surfaces, do; the platform’s defaults are already right.

Reusable style: the Modifier trait

There’s no stylesheet language. Reuse is ordinary Rust (a function or a Modifier, which is anything that maps a Piece to a decorated Piece):

pub struct Card;

impl Modifier for Card {
    fn apply(self, content: AnyPiece) -> AnyPiece {
        content.padding(16.0).background(CARD_BG).corner_radius(12.0)
    }
}

column((label("Plan"), label("Pro"))).modifier(Card)

Any FnOnce(AnyPiece) -> AnyPiece is a Modifier too, so one-off wrappers don’t need a named type. For app-wide theming, combine this with environment context:

with_environment(Palette::dark(), || {
    // Anywhere below: let palette: Palette = cx.use_context().unwrap();
    home_page()
})

Per-platform divergence

Sometimes the right style differs per platform: denser padding on desktop, larger touch targets on mobile. Today you branch on the compiled toolkit, which is a process constant and costs nothing at runtime:

let pad = if cfg!(feature = "uikit") || cfg!(feature = "mdc") { 16.0 } else { 10.0 };
content.padding(pad)

The design describes a tidier per_toolkit(12.0).uikit(16.0).qt(8.0) value type for this; it’s specified but not yet implemented, so cfg! branches are the current idiom. Either way the philosophy is the same: where platforms diverge, Day gives you a targeted override rather than pretending the divergence away.

Piece-specific style hooks exist where a control has real variants (button(...).style(...) takes a ButtonStyle, selector(...).style(SelectorStyle::Sidebar) picks sidebar vs. tab presentation), and these map to native variants, not custom drawing.

What you can’t restyle (on purpose)

There is no portable API to recolor a slider track, restyle a scrollbar, or reshape a checkbox. If a property can’t be honored by a toolkit, Day logs it once in debug rather than silently approximating it with custom drawing. That constraint is the flip side of every Day control behaving, and updating with the OS, exactly like a native one.

When a specific platform offers the knob you want (an AppKit bezel style, XAML tick marks), tweaks reach the real native widget and set it, per toolkit, without leaving the native-widget premise. And when you truly need fully custom visuals, that’s what canvas and composite pieces are for: draw your own leaf, keep native behavior around it.

If your product requires a heavily branded design system on every pixel (custom controls everywhere, identical on all platforms), a renderer-based framework will fight you less. Day is for apps that want to look like they belong on each platform. That choice is the subject of Why Day.


Next: the Guides cover the everyday tasks (navigation, localization, accessibility, testing), or jump to the API tour for the whole surface at a glance.