Theming
The components read their colors and radius from design tokens, which a theme sets for the page and a style sets for one element.
Tokens
The token module has one function per token, named as in shadcn/ui: background, foreground, primary, muted, border, ring, radius, and the rest. Each returns a Token, which is a CSS custom property with a typed default. A page that sets nothing gets the light values.
use hypersig::style::value::{Color, rem};
use hypersig::style::{Media, Style, Theme};
use hypersig::{Page, Signals};
use hypersig_ui::{ButtonSize, ButtonVariant, button, token};
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
struct App {
saved: bool,
}
// A blue primary color, lighter in the dark theme, and rounder corners.
let brand = Theme::new()
.set(&token::primary(), Color::hex(0x0025_63eb))
.set(&token::radius(), rem(1))
.on(Media::Dark, Theme::new().set(&token::primary(), Color::hex(0x0060_a5fa)));
let html = Page::new("App", App { saved: false })
.global(hypersig_ui::global())
.theme(hypersig_ui::theme())
.theme(brand)
.child(
button::<App>(ButtonVariant::Default, ButtonSize::Default)
// One element can set a token for itself and what is inside it.
.css(Style::new().token(&token::primary(), Color::hex(0x0016_a34a)))
.text("Save"),
)
.render();
assert!(html.contains("--primary:#2563eb"));Light and dark
hypersig_ui::theme() sets the dark values under prefers-color-scheme: dark. To let a signal pick the scheme instead, put hypersig_ui::dark() or hypersig_ui::light() on an element with css_when or css_match. Each sets every color token for the element and those inside it, and color-scheme with them, so native controls such as a scrollbar follow.
Result
theme.rs
use hypersig::html::div;
use hypersig::style::Style;
use hypersig::style::pattern::stack;
use hypersig::style::value::rem;
use hypersig::{Page, Signals};
use hypersig_ui::{
ButtonSize, ButtonVariant, Level, button, card, card_content, card_description, card_header,
card_title, select_field, switch, token,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq)]
pub enum Scheme {
System,
Light,
Dark,
}
#[derive(Signals, Serialize, Deserialize)]
pub struct Preview {
scheme: Scheme,
on: bool,
}
pub fn page() -> Page<Preview> {
let initial = Preview {
scheme: Scheme::System,
on: true,
};
Page::new(
"Preview",
initial,
)
.child(
div()
.css(
stack(rem(1))
.padding(rem(1))
.background_color(token::background().var())
.color(token::foreground().var()),
)
.css_match(
Preview::scheme(),
[
(Scheme::Light, hypersig_ui::light()),
(Scheme::Dark, hypersig_ui::dark()),
],
)
.child(
div()
.css(Style::new().max_width(rem(12)))
.child(select_field(
"Theme",
Preview::scheme(),
[
(
Scheme::System,
"System",
),
(
Scheme::Light,
"Light",
),
(
Scheme::Dark,
"Dark",
),
],
)),
)
.child(
card()
.child(
card_header()
.child(card_title(Level::H2).text("Notifications"))
.child(card_description().text("Choose what reaches your inbox.")),
)
.child(
card_content()
.css(stack(rem(1)))
.child(switch(
"Product updates",
Preview::on(),
))
.child(div().child(
button(ButtonVariant::Default, ButtonSize::Sm).text("Save"),
)),
),
),
)
}The tokens are plain values of
hypersig::style. Tokens and themes covers how they are written to the page.