hypersig::style, from the hypersig_style crate
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.
The properties, their keywords, the pseudo-classes, and the named colors come from MDN's CSS data, so the methods follow the standard.
As in Panda CSS and Tailwind CSS, each declaration is one class, and the page writes only the rules its elements use.
css_when and css_match switch styles with the page's signals, and a patch includes the rules of the elements it adds.
The swatches read one token, and bind_token sets it from the radio group's signal. Tokens and themes explains it.
Result
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"),
),
),
)
}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.
| hypersig | Tailwind CSS | Panda CSS | StyleX | vanilla-extract | Hand-written CSS | |
|---|---|---|---|---|---|---|
| CSS (gzip) | 1.0 KB | 1.6 KB | 1.0 KB | 1.0 KB | 0.6 KB | 0.8 KB |
| HTML without CSS (gzip) | 30.2 KB | 29.1 KB | 26.9 KB | 26.8 KB | 27.6 KB | 27.3 KB |
| Sent in all (gzip) | 31.5 KB | 30.7 KB | 27.8 KB | 27.8 KB | 28.2 KB | 28.1 KB |
| Build of the CSS | None | 268 ms | 574 ms | 182 ms | 74 ms | 3.1 ms |
| Server render | 1.7 ms | 1.5 ms | 1.4 ms | 1.4 ms | 0.8 ms | 0.8 ms |
| First Contentful Paint | 136 ms | 140 ms | 136 ms | 144 ms | 140 ms | 140 ms |
| Style and layout on load | 76 ms | 74 ms | 73 ms | 81 ms | 67 ms | 68 ms |
| Marking 2000 items done | 27 ms | 37 ms | 26 ms | 27 ms | 24 ms | 24 ms |
| Where hypersig falls behind | Why |
|---|---|
| HTML size | Each 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 render | Page::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.
The crate is reached as hypersig::style, and an El takes a Style with css. The components of hypersig_ui are styled with it.