Design System Starter Template
Copy-pasteable starter files for a dwind design system crate. Customize the color mappings, spacing scale, and typography roles for your project.
Cargo.toml
[package]
name = "my-design-system"
version = "0.1.0"
edition = "2021"
[dependencies]
dwind = { git = "https://github.com/nicksenger/dominator-css-bindgen", features = ["default_colors"] }
dwind-macros = { git = "https://github.com/nicksenger/dominator-css-bindgen" }
dominator = "0.5"
futures-signals = "0.3"
web-sys = { version = "0.3", features = ["HtmlElement", "CssStyleDeclaration", "Document", "Window", "Element"] }
wasm-bindgen = "0.2"
[build-dependencies]
dominator-css-bindgen = { git = "https://github.com/nicksenger/dominator-css-bindgen" }
tokens.css
/* ============================================================
PRIMITIVE TOKENS — raw values, named after what they ARE
============================================================ */
/* Spacing (4px base unit) */
.ds-space-1 { --ds-space-xs: 4px; }
.ds-space-2 { --ds-space-sm: 8px; }
.ds-space-4 { --ds-space-md: 16px; }
.ds-space-6 { --ds-space-lg: 24px; }
.ds-space-8 { --ds-space-xl: 32px; }
.ds-space-12 { --ds-space-2xl: 48px; }
/* Type sizes */
.ds-text-xs { --ds-text-label: 12px; }
.ds-text-sm { --ds-text-caption: 14px; }
.ds-text-base { --ds-text-body: 16px; }
.ds-text-xl { --ds-text-heading3: 20px; }
.ds-text-2xl { --ds-text-heading2: 24px; }
.ds-text-4xl { --ds-text-heading1: 36px; }
/* ============================================================
SEMANTIC TOKENS — purpose-based, named after what they DO
Dark mode is the default (:root)
============================================================ */
/* Backgrounds */
.ds-bg { color: var(--ds-color-bg); }
.ds-bg-elevated { color: var(--ds-color-bg-elevated); }
.ds-bg-muted { color: var(--ds-color-bg-muted); }
/* Text */
.ds-text { color: var(--ds-color-text); }
.ds-text-muted { color: var(--ds-color-text-muted); }
.ds-text-inverted { color: var(--ds-color-text-inverted); }
/* Interactive */
.ds-primary { color: var(--ds-color-primary); }
.ds-primary-hover { color: var(--ds-color-primary-hover); }
.ds-secondary { color: var(--ds-color-secondary); }
/* Borders */
.ds-border { border-color: var(--ds-color-border); }
.ds-border-muted { border-color: var(--ds-color-border-muted); }
/* Status */
.ds-success { color: var(--ds-color-success); }
.ds-warning { color: var(--ds-color-warning); }
.ds-error { color: var(--ds-color-error); }
.ds-info { color: var(--ds-color-info); }
theme/palettes.rs
#![allow(unused)] fn main() { /// Color values for each theme. Reference dwind's color palette /// (see dwind-styling references/color-palette.md for hex values). pub struct Palette { pub bg: &'static str, pub bg_elevated: &'static str, pub bg_muted: &'static str, pub text: &'static str, pub text_muted: &'static str, pub text_inverted: &'static str, pub primary: &'static str, pub primary_hover: &'static str, pub secondary: &'static str, pub border: &'static str, pub border_muted: &'static str, pub success: &'static str, pub warning: &'static str, pub error: &'static str, pub info: &'static str, } pub const DARK: Palette = Palette { bg: "#111827", // gray-900 bg_elevated: "#1f2937", // gray-800 bg_muted: "#374151", // gray-700 text: "#f9fafb", // gray-50 text_muted: "#9ca3af", // gray-400 text_inverted: "#111827", // gray-900 primary: "#3b82f6", // blue-500 primary_hover: "#2563eb", // blue-600 secondary: "#8b5cf6", // purple-500 border: "#374151", // gray-700 border_muted: "#1f2937", // gray-800 success: "#4ade80", // green-400 warning: "#fbbf24", // yellow-400 error: "#f87171", // red-400 info: "#38bdf8", // blue-400 (picton-blue) }; pub const LIGHT: Palette = Palette { bg: "#f9fafb", // gray-50 bg_elevated: "#ffffff", // white bg_muted: "#f3f4f6", // gray-100 text: "#111827", // gray-900 text_muted: "#6b7280", // gray-500 text_inverted: "#f9fafb", // gray-50 primary: "#2563eb", // blue-600 (darker for contrast on light bg) primary_hover: "#1d4ed8", // blue-700 secondary: "#7c3aed", // purple-600 border: "#e5e7eb", // gray-200 border_muted: "#f3f4f6", // gray-100 success: "#16a34a", // green-600 warning: "#d97706", // yellow-600 error: "#dc2626", // red-600 info: "#0284c7", // blue-600 }; }
theme/mod.rs
#![allow(unused)] fn main() { pub mod palettes; use palettes::{Palette, DARK, LIGHT}; use web_sys::wasm_bindgen::JsCast; #[derive(Clone, Copy, Debug, PartialEq)] pub enum Theme { Dark, Light, } impl Theme { pub fn palette(&self) -> &'static Palette { match self { Theme::Dark => &DARK, Theme::Light => &LIGHT, } } } /// Apply a theme by setting CSS custom properties on :root. /// Call this on app init and when the user switches themes. pub fn apply_theme(theme: Theme) { let Some(root) = web_sys::window() .and_then(|w| w.document()) .and_then(|d| d.document_element()) else { return; }; let el: &web_sys::HtmlElement = root.unchecked_ref(); let style = el.style(); let p = theme.palette(); let vars = [ ("--ds-color-bg", p.bg), ("--ds-color-bg-elevated", p.bg_elevated), ("--ds-color-bg-muted", p.bg_muted), ("--ds-color-text", p.text), ("--ds-color-text-muted", p.text_muted), ("--ds-color-text-inverted", p.text_inverted), ("--ds-color-primary", p.primary), ("--ds-color-primary-hover", p.primary_hover), ("--ds-color-secondary", p.secondary), ("--ds-color-border", p.border), ("--ds-color-border-muted", p.border_muted), ("--ds-color-success", p.success), ("--ds-color-warning", p.warning), ("--ds-color-error", p.error), ("--ds-color-info", p.info), ]; for (prop, val) in vars { let _ = style.set_property(prop, val); } // Toggle .light class for is(.light) selectors in dwclass! let class_list = root.class_list(); match theme { Theme::Dark => { let _ = class_list.remove_1("light"); }, Theme::Light => { let _ = class_list.add_1("light"); }, } } }
mixins/typography.rs
#![allow(unused)] fn main() { use dominator::DomBuilder; use web_sys::HtmlElement; /// Apply heading typography. Levels: 1 (largest) through 3. pub fn heading_text(level: u8) -> impl FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement> { move |b| { let b = match level { 1 => b.dwclass!("text-4xl font-bold leading-tight"), 2 => b.dwclass!("text-2xl font-bold leading-tight"), 3 => b.dwclass!("text-xl font-semibold leading-snug"), _ => b.dwclass!("text-lg font-semibold leading-snug"), }; b.style("color", "var(--ds-color-text)") } } /// Body text — default paragraph styling. pub fn body_text() -> impl FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement> { |b| { b.dwclass!("text-base font-normal leading-normal") .style("color", "var(--ds-color-text)") } } /// Caption text — secondary information, metadata. pub fn caption_text() -> impl FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement> { |b| { b.dwclass!("text-sm font-normal leading-normal") .style("color", "var(--ds-color-text-muted)") } } /// Label text — form labels, badges, small UI text. pub fn label_text() -> impl FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement> { |b| { b.dwclass!("text-xs font-medium leading-normal") .style("color", "var(--ds-color-text-muted)") } } }
mixins/surfaces.rs
#![allow(unused)] fn main() { use dominator::DomBuilder; use web_sys::HtmlElement; /// Standard card surface with themed background and border. pub fn card_surface() -> impl FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement> { |b| { b.dwclass!("rounded-lg") .style("background", "var(--ds-color-bg-elevated)") .style("border", "1px solid var(--ds-color-border-muted)") .style("padding", "var(--ds-space-md)") } } /// Elevated surface with shadow for modals, dropdowns, popovers. pub fn elevated_surface() -> impl FnOnce(DomBuilder<HtmlElement>) -> DomBuilder<HtmlElement> { |b| { b.dwclass!("rounded-lg shadow-xl") .style("background", "var(--ds-color-bg-elevated)") .style("border", "1px solid var(--ds-color-border-muted)") .style("padding", "var(--ds-space-lg)") } } }
build.rs
use dominator_css_bindgen::css::generate_rust_bindings_from_file; use std::path::PathBuf; fn main() { let out_dir = PathBuf::from(std::env::var("OUT_DIR").unwrap()); let css_dir = PathBuf::from("resources/css"); generate_rust_bindings_from_file( &css_dir.join("tokens.css"), &out_dir.join("tokens.rs"), ); println!("cargo:rerun-if-changed=resources/css/"); }
lib.rs
#![allow(unused)] fn main() { #[macro_use] extern crate dwind_macros; pub mod theme; pub mod mixins; mod tokens_css { include!(concat!(env!("OUT_DIR"), "/tokens.rs")); } /// Call once at app startup before rendering any components. pub fn init_design_system() { dwind::stylesheet(); tokens_css::init_styles(); theme::apply_theme(theme::Theme::Dark); } }
Usage
#![allow(unused)] fn main() { use my_design_system::{init_design_system, theme, mixins::typography::*}; use dominator::{html, Dom}; fn app() -> Dom { init_design_system(); html!("div", { .style("background", "var(--ds-color-bg)") .style("min-height", "100vh") .dwclass!("flex flex-col gap-6 p-6") .child(html!("h1", { .apply(heading_text(1)) .text("Welcome") })) .child(html!("p", { .apply(body_text()) .text("This uses your design system tokens.") })) }) } }