日本語

hypersig::style, from the hypersig_style crate

Typed atomic CSS, written in Rust

Every standard CSS property is a method, and every keyword a variant. A misspelled property or a value of the wrong type is a compile error, and each declaration becomes one class.

Generated from MDN

The properties, their keywords, the pseudo-classes, and the named colors come from MDN's CSS data, so the methods follow the standard.

One class per declaration

As in Panda CSS and Tailwind CSS, each declaration is one class, and the page writes only the rules its elements use.

Changes with the signals

css_when and css_match switch styles with the page's signals, and a patch includes the rules of the elements it adds.

A token from a signal

The swatches read one token, and bind_token sets it from the radio group's signal. Tokens and themes explains it.

Result

accent.rs
use hypersig::html::div;
use hypersig::style::pattern::{stack, wrap};
use hypersig::style::value::{Color, px, rem};
use hypersig::style::{Style, Token};
use hypersig::{Page, Signals};
use hypersig_ui::{ButtonSize, ButtonVariant, button, radio_group, token};
use serde::{Deserialize, Serialize};

#[derive(Signals, Serialize, Deserialize)]
pub struct Palette {
    accent: String,
}

pub fn page() -> Page<Palette> {
    let accent = Token::new("accent-demo", Color::hex(0x0025_63eb));
    let swatch = Style::new()
        .width(rem(3))
        .height(rem(3))
        .border_radius(px(8))
        .background_color(accent.var());
    Page::new(
        "Palette",
        Palette {
            accent: "#2563eb".into(),
        },
    )
    .child(
        div()
            .css(stack(rem(1)))
            .bind_token(&accent, Palette::accent())
            .child(radio_group(
                "Accent",
                Palette::accent(),
                [
                    (
                        "#2563eb".to_owned(),
                        "Blue",
                    ),
                    (
                        "#16a34a".to_owned(),
                        "Green",
                    ),
                    (
                        "#e11d48".to_owned(),
                        "Rose",
                    ),
                ],
            ))
            .child(
                div()
                    .css(wrap(rem(0.75)))
                    .child(div().css(swatch.clone()))
                    .child(div().css(swatch.border_radius(px(999))))
                    .child(
                        button(ButtonVariant::Default, ButtonSize::Default)
                            // The button reads `primary`, which takes the accent here.
                            .css(Style::new().token(&token::primary(), accent.var()))
                            .text("Accent button"),
                    ),
            ),
    )
}

Benchmarks

The 2000-item TodoMVC page of hypersig's framework benchmark, styled six ways and rendered by hypersig with the same markup, so only the class names and the CSS differ. Measured in Chrome 155 on an Apple M4 Max with the CPU slowed 4x. Browser values are medians of 5 runs, and the server render is the median of 50.

hypersigTailwind CSSPanda CSSStyleXvanilla-extractHand-written CSS
CSS (gzip)1.0 KB1.6 KB1.0 KB1.0 KB0.6 KB0.8 KB
HTML without CSS (gzip)30.2 KB29.1 KB26.9 KB26.8 KB27.6 KB27.3 KB
Sent in all (gzip)31.5 KB30.7 KB27.8 KB27.8 KB28.2 KB28.1 KB
Build of the CSSNone268 ms574 ms182 ms74 ms3.1 ms
Server render1.7 ms1.5 ms1.4 ms1.4 ms0.8 ms0.8 ms
First Contentful Paint136 ms140 ms136 ms144 ms140 ms140 ms
Style and layout on load76 ms74 ms73 ms81 ms67 ms68 ms
Marking 2000 items done27 ms37 ms26 ms27 ms24 ms24 ms
Where hypersig falls behindWhy
HTML sizeEach class name is made from its declaration alone: the conditions, the property's full name, and the value, as in font-size_24px. A value with a space or another character outside letters, digits, ., #, %, and - becomes a 16-digit hash. A server patch renders new elements without knowing the page's classes, so one declaration has to get the same name every time it is rendered
Server renderPage::render writes those names into every item. The HTML before gzip is 1.21 MB for hypersig and 0.88 MB for Panda CSS, and the render times grow in the same order

The pages and the harness are in bench/style.

Part of hypersig

The crate is reached as hypersig::style, and an El takes a Style with css. The components of hypersig_ui are styled with it.