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

Themes and styling

Iced lets you customize the look and feel of your app on multiple levels:

  • Singular widget instances can have unique looks.
  • Themes can define their own colorschemes and base widget styles, which are used by default.

Themes

You can set the theme used by your app using Application::theme:

fn main() -> iced::Result {
    iced::application(...)
        .theme(|_| iced::Theme::Dracula)
        .run()
}

A nicer way to do this is by storing the theme in your app struct, then implementing a theme method (or something similar) for it which looks something like this:

use iced::{Element, Theme, widget::pick_list};

fn main() -> iced::Result {
    iced::application(App::default, App::update, App::view)
        .theme(App::theme)
        .run()
}

#[derive(Default)]
struct App {
    theme: Option<Theme>,
}

#[derive(Debug, Clone)]
enum Message {
    ThemeChanged(Theme),
}

impl App {
    fn theme(&self) -> Option<Theme> {
        self.theme.clone()
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::ThemeChanged(theme) => self.theme = Some(theme),
        };
    }

    fn view(&self) -> Element<'_, Message> {
        pick_list(Theme::ALL, self.theme.clone(), Message::ThemeChanged)
            .placeholder("Choose a theme...")
            .into()
    }
}

NOTE: Returning Option<Theme> allows us to fall back to the default theme (this depends on the user's OS settings) when we return None.

This is reactive, i.e. changing that preference will cause our app to react as well, so long as we still return None.

Then you can use it like so:

use iced::{Element, Theme, widget::pick_list};

fn main() -> iced::Result {
    iced::application(App::default, App::update, App::view)
        .theme(App::theme)
        .run()
}

#[derive(Default)]
struct App {
    theme: Option<Theme>,
}

#[derive(Debug, Clone)]
enum Message {
    ThemeChanged(Theme),
}

impl App {
    fn theme(&self) -> Option<Theme> {
        self.theme.clone()
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::ThemeChanged(theme) => self.theme = Some(theme),
        };
    }

    fn view(&self) -> Element<'_, Message> {
        pick_list(Theme::ALL, self.theme.clone(), Message::ThemeChanged)
            .placeholder("Choose a theme...")
            .into()
    }
}

Complete example

main.rs

use iced::{Element, Theme, widget::pick_list};

fn main() -> iced::Result {
    iced::application(App::default, App::update, App::view)
        .theme(App::theme)
        .run()
}

#[derive(Default)]
struct App {
    theme: Option<Theme>,
}

#[derive(Debug, Clone)]
enum Message {
    ThemeChanged(Theme),
}

impl App {
    fn theme(&self) -> Option<Theme> {
        self.theme.clone()
    }

    fn update(&mut self, message: Message) {
        match message {
            Message::ThemeChanged(theme) => self.theme = Some(theme),
        };
    }

    fn view(&self) -> Element<'_, Message> {
        pick_list(Theme::ALL, self.theme.clone(), Message::ThemeChanged)
            .placeholder("Choose a theme...")
            .into()
    }
}

Custom themes

The built-in iced::Theme type has a Custom variant which can be created using Theme::custom (or Theme::custom_with_fn for greater control over the generated color palette).

If you need even more customization, you can create your own Theme type. The requirements are:

  • Implement Base for your custom type.
  • For each widget you plan to support, implement its Catalog trait (if it has one) and the dependencies of that trait.

For a reference custom theme, see iced_material.

Styling

Per-widget styling is done via styling methods. These take &Theme (usually) and return a widget Style. The following examples are made with the button widget in mind, but most things apply to other widgets as well.

Most widgets include a default styling method, but some also have extras. You can use these by passing them to the widget's style method:

use iced::{
    Element, Theme, border, color,
    widget::{button, row},
};

fn main() -> iced::Result {
    iced::run(App::update, App::view)
}

#[derive(Default)]
struct App;

#[derive(Debug, Clone)]
enum Message {
    Noop,
}

impl App {
    fn update(&mut self, message: Message) {
        match message {
            Message::Noop => {}
        }
    }

    fn view(&self) -> Element<'_, Message> {
        row![
            button("Built-in 'danger' style")
                .on_press(Message::Noop)
                .style(button::danger),
            button("User defined inline style")
                .on_press(Message::Noop)
                .style(|_, _| button::Style {
                    background: Some(color!(0x1e1e2e).into()),
                    text_color: color!(0xc0ffee),
                    border: border::rounded(10),
                    ..Default::default()
                }),
            button("User defined style method")
                .on_press(Message::Noop)
                .style(button_danger_text)
        ]
        .spacing(10)
        .into()
    }
}

// This is a *slightly* modified version of button::text
fn button_danger_text(theme: &Theme, status: button::Status) -> button::Style {
    let palette = theme.extended_palette();

    let base = button::Style {
        text_color: palette.danger.base.color,
        ..button::Style::default()
    };

    match status {
        button::Status::Active | button::Status::Pressed => base,
        button::Status::Hovered => button::Style {
            text_color: palette.danger.base.color.scale_alpha(0.8),
            ..base
        },
        button::Status::Disabled => button::Style {
            text_color: palette.danger.base.color.scale_alpha(0.5),
            ..base
        }
    }
}

You can also easily create static (or even dynamic) inline styles:

use iced::{
    Element, Theme, border, color,
    widget::{button, row},
};

fn main() -> iced::Result {
    iced::run(App::update, App::view)
}

#[derive(Default)]
struct App;

#[derive(Debug, Clone)]
enum Message {
    Noop,
}

impl App {
    fn update(&mut self, message: Message) {
        match message {
            Message::Noop => {}
        }
    }

    fn view(&self) -> Element<'_, Message> {
        row![
            button("Built-in 'danger' style")
                .on_press(Message::Noop)
                .style(button::danger),
            button("User defined inline style")
                .on_press(Message::Noop)
                .style(|_, _| button::Style {
                    background: Some(color!(0x1e1e2e).into()),
                    text_color: color!(0xc0ffee),
                    border: border::rounded(10),
                    ..Default::default()
                }),
            button("User defined style method")
                .on_press(Message::Noop)
                .style(button_danger_text)
        ]
        .spacing(10)
        .into()
    }
}

// This is a *slightly* modified version of button::text
fn button_danger_text(theme: &Theme, status: button::Status) -> button::Style {
    let palette = theme.extended_palette();

    let base = button::Style {
        text_color: palette.danger.base.color,
        ..button::Style::default()
    };

    match status {
        button::Status::Active | button::Status::Pressed => base,
        button::Status::Hovered => button::Style {
            text_color: palette.danger.base.color.scale_alpha(0.8),
            ..base
        },
        button::Status::Disabled => button::Style {
            text_color: palette.danger.base.color.scale_alpha(0.5),
            ..base
        }
    }
}

Notice the two underscores? They're for the &Theme and Status that get passed to our closure. What's this Status, you ask? Well, a button may be hovered at a given moment, or it could be disabled, be pressed down, or neither. Taking this into account, lets see how we can create a dynamic styling method:

use iced::{
    Element, Theme, border, color,
    widget::{button, row},
};

fn main() -> iced::Result {
    iced::run(App::update, App::view)
}

#[derive(Default)]
struct App;

#[derive(Debug, Clone)]
enum Message {
    Noop,
}

impl App {
    fn update(&mut self, message: Message) {
        match message {
            Message::Noop => {}
        }
    }

    fn view(&self) -> Element<'_, Message> {
        row![
            button("Built-in 'danger' style")
                .on_press(Message::Noop)
                .style(button::danger),
            button("User defined inline style")
                .on_press(Message::Noop)
                .style(|_, _| button::Style {
                    background: Some(color!(0x1e1e2e).into()),
                    text_color: color!(0xc0ffee),
                    border: border::rounded(10),
                    ..Default::default()
                }),
            button("User defined style method")
                .on_press(Message::Noop)
                .style(button_danger_text)
        ]
        .spacing(10)
        .into()
    }
}

// This is a *slightly* modified version of button::text
fn button_danger_text(theme: &Theme, status: button::Status) -> button::Style {
    let palette = theme.extended_palette();

    let base = button::Style {
        text_color: palette.danger.base.color,
        ..button::Style::default()
    };

    match status {
        button::Status::Active | button::Status::Pressed => base,
        button::Status::Hovered => button::Style {
            text_color: palette.danger.base.color.scale_alpha(0.8),
            ..base
        },
        button::Status::Disabled => button::Style {
            text_color: palette.danger.base.color.scale_alpha(0.5),
            ..base
        }
    }
}
use iced::{
    Element, Theme, border, color,
    widget::{button, row},
};

fn main() -> iced::Result {
    iced::run(App::update, App::view)
}

#[derive(Default)]
struct App;

#[derive(Debug, Clone)]
enum Message {
    Noop,
}

impl App {
    fn update(&mut self, message: Message) {
        match message {
            Message::Noop => {}
        }
    }

    fn view(&self) -> Element<'_, Message> {
        row![
            button("Built-in 'danger' style")
                .on_press(Message::Noop)
                .style(button::danger),
            button("User defined inline style")
                .on_press(Message::Noop)
                .style(|_, _| button::Style {
                    background: Some(color!(0x1e1e2e).into()),
                    text_color: color!(0xc0ffee),
                    border: border::rounded(10),
                    ..Default::default()
                }),
            button("User defined style method")
                .on_press(Message::Noop)
                .style(button_danger_text)
        ]
        .spacing(10)
        .into()
    }
}

// This is a *slightly* modified version of button::text
fn button_danger_text(theme: &Theme, status: button::Status) -> button::Style {
    let palette = theme.extended_palette();

    let base = button::Style {
        text_color: palette.danger.base.color,
        ..button::Style::default()
    };

    match status {
        button::Status::Active | button::Status::Pressed => base,
        button::Status::Hovered => button::Style {
            text_color: palette.danger.base.color.scale_alpha(0.8),
            ..base
        },
        button::Status::Disabled => button::Style {
            text_color: palette.danger.base.color.scale_alpha(0.5),
            ..base
        }
    }
}

Complete example

main.rs

use iced::{
    Element, Theme, border, color,
    widget::{button, row},
};

fn main() -> iced::Result {
    iced::run(App::update, App::view)
}

#[derive(Default)]
struct App;

#[derive(Debug, Clone)]
enum Message {
    Noop,
}

impl App {
    fn update(&mut self, message: Message) {
        match message {
            Message::Noop => {}
        }
    }

    fn view(&self) -> Element<'_, Message> {
        row![
            button("Built-in 'danger' style")
                .on_press(Message::Noop)
                .style(button::danger),
            button("User defined inline style")
                .on_press(Message::Noop)
                .style(|_, _| button::Style {
                    background: Some(color!(0x1e1e2e).into()),
                    text_color: color!(0xc0ffee),
                    border: border::rounded(10),
                    ..Default::default()
                }),
            button("User defined style method")
                .on_press(Message::Noop)
                .style(button_danger_text)
        ]
        .spacing(10)
        .into()
    }
}

// This is a *slightly* modified version of button::text
fn button_danger_text(theme: &Theme, status: button::Status) -> button::Style {
    let palette = theme.extended_palette();

    let base = button::Style {
        text_color: palette.danger.base.color,
        ..button::Style::default()
    };

    match status {
        button::Status::Active | button::Status::Pressed => base,
        button::Status::Hovered => button::Style {
            text_color: palette.danger.base.color.scale_alpha(0.8),
            ..base
        },
        button::Status::Disabled => button::Style {
            text_color: palette.danger.base.color.scale_alpha(0.5),
            ..base
        }
    }
}