Skip to content

Instantly share code, notes, and snippets.

@airstrike
Created August 26, 2026 13:18
Show Gist options
  • Select an option

  • Save airstrike/895995c8e608ebafc82dec4e5207cd2b to your computer and use it in GitHub Desktop.

Select an option

Save airstrike/895995c8e608ebafc82dec4e5207cd2b to your computer and use it in GitHub Desktop.
elevato theme as a drop-in theme.rs for iced 0.14 - Mode/Seed, semantic Palette, named style fns for text/container/button/text_editor
//! elevato theme - warm paper, ink text, one lamp-green accent, all mono.
//!
//! The theme behind <https://elevato.rs>, lifted out as a single file you
//! can drop into an iced project. Copy to `src/theme.rs`, add `mod theme;`,
//! then:
//!
//! ```ignore
//! // in your application setup
//! .theme(|state| state.mode.to_theme())
//! .default_font(theme::MONO)
//!
//! // at call sites - always a named fn from this module, never an
//! // inline style closure
//! button("Start").style(theme::button::primary)
//! container(pane).style(theme::container::pane)
//! text("label").style(theme::text::secondary)
//! ```
//!
//! Two layers:
//!
//! - [`Mode::to_theme`] builds an iced [`Theme`] from six seed colors, so
//! stock widgets (pick_list, slider, the text ladders) inherit the
//! design instead of iced's defaults.
//! - [`palette`] resolves a hand-picked [`Palette`] of named semantic
//! colors for the current theme's brightness - for canvases and the
//! style fns below, which cover surfaces beyond what a seed expresses.
//!
//! Solid colors only: gradients silently no-op on the wasm build.
//!
//! Requires iced 0.14+ (`Theme::custom(name, Seed)`, `border::rounded`).
//! On wasm, [`Mode::from_env`] reads `prefers-color-scheme` through
//! `web-sys` (features `Window`, `MediaQueryList`); natively it reads
//! the `THEME` env var. Delete `from_env` if you'd rather decide yourself.
//!
//! The face is Geist Mono (OFL). It is not bundled here - embed it with
//! `include_bytes!` and register it via `.font(...)`, or point [`MONO`]
//! at whatever monospace you ship.
//!
//! MIT - Andy Terra
use iced::theme::palette::Seed;
use iced::{Color, Font, Theme, color};
/// Light or dark, the app-level choice driving [`Mode::to_theme`].
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Mode {
Light,
#[default]
Dark,
}
impl Mode {
/// Reads `THEME=light|dark` natively; on the web, the browser's
/// `prefers-color-scheme`.
pub fn from_env() -> Self {
#[cfg(not(target_arch = "wasm32"))]
if let Ok(value) = std::env::var("THEME") {
if value.eq_ignore_ascii_case("light") {
return Self::Light;
}
}
#[cfg(target_arch = "wasm32")]
if web_sys::window()
.and_then(|window| window.match_media("(prefers-color-scheme: light)").ok())
.flatten()
.is_some_and(|query| query.matches())
{
return Self::Light;
}
Self::default()
}
/// The other mode.
pub fn toggle(self) -> Self {
match self {
Self::Dark => Self::Light,
Self::Light => Self::Dark,
}
}
/// Builds the iced theme from the seed colors, so stock widget
/// defaults inherit the design. Surface colors beyond the seed are
/// hand-picked in [`palette`].
pub fn to_theme(self) -> Theme {
let (name, seed) = match self {
Self::Light => (
"elevato",
Seed {
background: color!(0xf2efe9),
text: color!(0x2b2a27),
primary: color!(0x2e8c5f),
success: color!(0x2e7d43),
warning: color!(0xb08a2e),
danger: color!(0xc0473a),
},
),
Self::Dark => (
"elevato dark",
Seed {
background: color!(0x191816),
text: color!(0xece7db),
primary: color!(0x50b481),
success: color!(0x5fbf77),
warning: color!(0xd9c26a),
danger: color!(0xd95f4e),
},
),
};
Theme::custom(name.to_string(), seed)
}
}
/// The one face. Geist Mono if you ship it; otherwise iced's monospace.
pub const MONO: Font = Font::MONOSPACE;
/// Named semantic colors, resolved per theme brightness by [`palette`].
///
/// The names are generic where elevato's were literal: `surface` was
/// the elevator cab, `surface_strong` its header band, `figure` a
/// passenger, `line` a floor baseline.
#[derive(Debug, Clone, Copy)]
pub struct Palette {
/// Headline and value text.
pub text_primary: Color,
/// Labels and captions.
pub text_secondary: Color,
/// The world: canvas or page backdrop.
pub background: Color,
/// Toolbar, stats bar, and banner surfaces; the shade behind panes.
pub panel: Color,
/// Hairline borders and baselines.
pub line: Color,
/// A raised object drawn on the canvas.
pub surface: Color,
/// A stronger band on that object.
pub surface_strong: Color,
/// Text drawn on `surface`.
pub text_on_surface: Color,
/// A lit indicator: the accent, on.
pub accent: Color,
/// An unlit indicator: the accent, off.
pub accent_off: Color,
/// A lit button that should read warm rather than green.
pub highlight: Color,
/// Figures (people) on the canvas.
pub figure: Color,
/// Success green (banner text).
pub success: Color,
/// Failure red (banner text, errors).
pub failure: Color,
/// Editor tokens: comments.
pub syntax_comment: Color,
/// Editor tokens: string literals.
pub syntax_string: Color,
/// Editor tokens: numeric literals.
pub syntax_number: Color,
/// Editor tokens: `true`/`false`.
pub syntax_constant: Color,
/// Editor tokens: operators.
pub syntax_operator: Color,
/// Editor tokens: keywords.
pub syntax_keyword: Color,
}
/// Resolves the semantic palette for the current theme's brightness.
pub fn palette(theme: &Theme) -> Palette {
if is_dark(theme) {
// Night lobby: near-black warm surfaces, lamp-green accents.
Palette {
text_primary: color!(0xece7db),
text_secondary: color!(0x98948a),
background: color!(0x191816),
panel: color!(0x22211e),
line: color!(0x35332e),
surface: color!(0x3b3a36),
surface_strong: color!(0x2e2d2a),
text_on_surface: color!(0xf0ecdf),
accent: color!(0x6fd692),
accent_off: color!(0x413f3a),
highlight: color!(0xf2eecf),
figure: color!(0xd4cfc2),
success: color!(0x5fbf77),
failure: color!(0xd95f4e),
syntax_comment: color!(0x8a867c),
syntax_string: color!(0x9ec978),
syntax_number: color!(0xc9a45c),
syntax_constant: color!(0x62c1c9),
syntax_operator: color!(0xaaa69b),
syntax_keyword: color!(0x50b481),
}
} else {
// Daylight lobby: warm paper, ink text, the same lamp-green.
Palette {
text_primary: color!(0x2b2a27),
text_secondary: color!(0x6f6c64),
background: color!(0xf2efe9),
panel: color!(0xe8e4da),
line: color!(0xcfcabd),
surface: color!(0xdad4c6),
surface_strong: color!(0xbfb9a8),
text_on_surface: color!(0x2b2a27),
accent: color!(0x2e8c5f),
accent_off: color!(0xbfbaab),
highlight: color!(0x8a6d1f),
figure: color!(0x3f3d37),
success: color!(0x2e7d43),
failure: color!(0xc0473a),
syntax_comment: color!(0x928d80),
syntax_string: color!(0x4e8d3e),
syntax_number: color!(0xa07617),
syntax_constant: color!(0x137e8a),
syntax_operator: color!(0x6f6c64),
syntax_keyword: color!(0x2e8c5f),
}
}
}
fn is_dark(theme: &Theme) -> bool {
let background = theme.palette().background.base.color;
0.299 * background.r + 0.587 * background.g + 0.114 * background.b < 0.5
}
pub mod text {
//! Text colors for `.style()` on `text` widgets.
use iced::Theme;
use iced::widget::text::Style;
use super::palette;
/// Headline and value text.
pub fn primary(theme: &Theme) -> Style {
Style {
color: Some(palette(theme).text_primary),
}
}
/// Labels and captions.
pub fn secondary(theme: &Theme) -> Style {
Style {
color: Some(palette(theme).text_secondary),
}
}
/// Error text.
pub fn failure(theme: &Theme) -> Style {
Style {
color: Some(palette(theme).failure),
}
}
/// Green on success, red on failure.
pub fn outcome(success: bool) -> impl Fn(&Theme) -> Style {
move |theme| {
let palette = palette(theme);
Style {
color: Some(if success {
palette.success
} else {
palette.failure
}),
}
}
}
}
pub mod container {
//! Surface styles for `.style()` on `container` widgets.
use iced::widget::container::Style;
use iced::{Theme, border};
use super::palette;
/// The root backdrop behind everything: the panel shade, so panes
/// (drawn in the background shade) read as raised cards against it.
pub fn root(theme: &Theme) -> Style {
let palette = palette(theme);
Style {
background: Some(palette.panel.into()),
text_color: Some(palette.text_primary),
..Style::default()
}
}
/// A workspace card. Give every pane the same rounded surface so
/// the halves of a split are twins.
pub fn pane(theme: &Theme) -> Style {
let palette = palette(theme);
Style {
background: Some(palette.background.into()),
border: border::rounded(8.0).color(palette.line).width(1),
..Style::default()
}
}
/// Toolbar and stats-bar strips.
pub fn panel(theme: &Theme) -> Style {
Style {
background: Some(palette(theme).panel.into()),
..Style::default()
}
}
/// A card floating over the canvas.
pub fn banner(theme: &Theme) -> Style {
let palette = palette(theme);
Style {
background: Some(palette.panel.into()),
border: border::rounded(8.0).color(palette.line).width(1),
..Style::default()
}
}
/// A strip that carries an error.
pub fn error_panel(theme: &Theme) -> Style {
let palette = palette(theme);
Style {
background: Some(palette.panel.into()),
border: border::rounded(4.0).color(palette.failure).width(1),
..Style::default()
}
}
}
pub mod text_editor {
//! Styles for `.style()` on `text_editor` widgets.
use iced::widget::text_editor::{Status, Style};
use iced::{Border, Color, Theme};
use super::palette;
/// A chromeless code editor: the pane it sits in provides the
/// surface and border, so editor and canvas dress identically.
pub fn code(theme: &Theme, _status: Status) -> Style {
let palette = palette(theme);
Style {
background: Color::TRANSPARENT.into(),
border: Border::default(),
placeholder: palette.text_secondary,
value: palette.text_primary,
selection: Color {
a: 0.35,
..palette.surface
},
}
}
}
pub mod button {
//! Button styles for `.style()` on `button` widgets: a deliberate
//! hierarchy - one filled `primary` per surface, `outline` for
//! secondary actions, `ghost` for tertiary ones.
use iced::widget::button::{Status, Style};
use iced::{Border, Theme, border};
use super::palette;
/// Filled accent - the surface's main action.
pub fn primary(theme: &Theme, status: Status) -> Style {
let ladder = theme.palette();
let base = Style {
background: Some(ladder.primary.base.color.into()),
text_color: ladder.primary.base.text,
border: border::rounded(6),
..Style::default()
};
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Style {
background: Some(ladder.primary.strong.color.into()),
..base
},
Status::Disabled => faded(base),
}
}
/// Outlined, quiet - secondary actions.
pub fn outline(theme: &Theme, status: Status) -> Style {
let palette = palette(theme);
let base = Style {
background: None,
text_color: palette.text_primary,
border: Border {
color: palette.line,
width: 1.0,
radius: 6.0.into(),
},
..Style::default()
};
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Style {
background: Some(palette.panel.into()),
border: Border {
color: palette.text_secondary,
..base.border
},
..base
},
Status::Disabled => faded(base),
}
}
/// Bare - tertiary actions.
pub fn ghost(theme: &Theme, status: Status) -> Style {
let palette = palette(theme);
let base = Style {
background: None,
text_color: palette.text_secondary,
border: border::rounded(6),
..Style::default()
};
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Style {
background: Some(palette.panel.into()),
text_color: palette.text_primary,
..base
},
Status::Disabled => faded(base),
}
}
/// [`ghost`], except a disabled button disappears entirely while
/// still occupying its space - for paired affordances (back/forward
/// arrows) whose layout must not shift as each end is reached.
pub fn vanishing(theme: &Theme, status: Status) -> Style {
match status {
Status::Disabled => Style {
background: None,
text_color: iced::Color::TRANSPARENT,
border: border::rounded(6),
..Style::default()
},
status => ghost(theme, status),
}
}
/// A text link: bare accent text that brightens on hover.
pub fn link(theme: &Theme, status: Status) -> Style {
let ladder = theme.palette();
let base = Style {
background: None,
text_color: ladder.primary.base.color,
border: border::rounded(6),
..Style::default()
};
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Style {
text_color: ladder.primary.strong.color,
..base
},
Status::Disabled => faded(base),
}
}
/// A tab: quiet when inactive, raised on the panel shade when active.
pub fn tab(active: bool) -> impl Fn(&Theme, Status) -> Style {
move |theme, status| {
let palette = palette(theme);
let base = if active {
Style {
background: Some(palette.panel.into()),
text_color: palette.text_primary,
border: border::rounded(6),
..Style::default()
}
} else {
Style {
background: None,
text_color: palette.text_secondary,
border: border::rounded(6),
..Style::default()
}
};
match status {
Status::Active | Status::Pressed => base,
Status::Hovered => Style {
text_color: palette.text_primary,
background: Some(palette.panel.into()),
..base
},
Status::Disabled => faded(base),
}
}
}
fn faded(base: Style) -> Style {
Style {
text_color: base.text_color.scale_alpha(0.4),
background: base
.background
.map(|background| background.scale_alpha(0.4)),
border: Border {
color: base.border.color.scale_alpha(0.4),
..base.border
},
..base
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment