css

package module
v0.4.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 30, 2026 License: MIT Imports: 1 Imported by: 0

README

tinywasm/css

Typed CSS design tokens and emission engine for the tinywasm framework.

This module acts as the single source of truth for design decisions and theme construction. It replaces string-based .css files with Go-typed design tokens, exposing both RootCSS() and RenderCSS() with strictly separate responsibilities:

  • RootCSS()vocabulary: design token declarations — brand, source tokens, scales.
  • RenderCSS()logic: minimal reset + active-token bindings + @media (prefers-color-scheme).

The internal DSL ensures that every selector, declaration, and token reference is a Go expression, providing compile-time safety and eliminating hex-fallback drift.

Public API & Architecture

To prevent design-system evasion, the low-level CSS properties and free-value constructors have been unexported from the public surface.

The public API consists solely of:

  • Token and Pair design tokens.
  • The design token catalog (e.g., ColorPrimary, ColorSurface, Space2, MixHover, etc.).
  • Hover(t), Focus(t), Press(t) — interaction derivations via color-mix().
  • Device, Mobile, Tablet, Desktop, Query(...) — typed viewport classes for media queries.
  • Stylesheet and NewStylesheet for compilation.
  • Theme, Set, SetTheme for rebranded app themes.

All component styling is expressed using the semantic visual intention API in github.com/tinywasm/widget/style, which compiles down to CSS rules.

Component Styling Example

Components do not write raw CSS or lower-level property functions. Instead, they express intent using high-level layouts (Stack, Row, Split), semantic surfaces (On(Page), On(Panel)), and scale exceptions (Fill(), Round(), Pad()):

package targetlist

import (
	"github.com/tinywasm/widget"
	"github.com/tinywasm/widget/style"
)

const (
	nameTargetList = widget.Name("targetlist")
	partRow        = widget.Part("row")
)

func (l *TargetList) WidgetName() widget.Name { return nameTargetList }
func (l *TargetList) WidgetKind() widget.Kind { return widget.Listbox }

func (l *TargetList) Style() *style.Sheet {
	return style.Of(nameTargetList).
		Root(style.Stack(style.Space1), style.On(style.Sunken), style.Scrolls()).
		Part(partRow, style.Row(style.Space2), style.On(style.Panel), style.Pad(style.Space2), style.Round(style.RadiusSm)).
		When(widget.Selected, partRow, style.On(style.Selected))
}

SSR contract: RootCSS vs RenderCSS

assetmin recognizes two CSS functions with strictly separate roles:

Function Slot Replacement Content
RootCSS() *Stylesheet open Single-winner — app replaces framework :root {} value declarations (vocabulary)
RenderCSS() *Stylesheet middle Additive — every module's contribution is preserved CSS rules that consume tokens via var() (logic)

The split is the key to safe theming: vocabulary is replaceable so apps can rebrand; logic is additive so dark-mode switching cannot be deleted by accident.

Theming an App (Rebrand)

To apply a theme or rebrand to an application, the root project exposes its own RootCSS(). Because assetmin treats the :root block as a single-winner slot, the app's RootCSS() completely replaces the library defaults.

// config/css.go in the application (!wasm)
import "github.com/tinywasm/css"

func RootCSS() *css.Stylesheet {
    return css.Theme(
        css.Set(css.ColorPrimary, "#FF6B35"),
        css.SetTheme(css.ColorBackground, "#FAFAFA", "#121212"),
        css.SetTheme(css.ColorSurface, "#F2F2F7", "#161B22"),
        css.Set(css.MixHover, "22%"),
    )
}

Theme() returns a stylesheet with the token declarations that the app needs to overwrite, appended at the end of the default catalog block. This ensures that the app's branding overrides cascade correctly.

The app does not need to redeclare active layer bindings (--color-surface, etc.) or @media (prefers-color-scheme) logic; those reside in RenderCSS() and remain always active.

| Action | API | |---|---|---| | Rebrand brand color | css.Set(css.ColorPrimary, "#hex") | | Change background (theme pair) | css.SetTheme(css.ColorBackground, "#light", "#dark") | | Adjust interaction intensity | css.Set(css.MixHover, "22%") | | Adjust global border-radius | css.Set(css.RadiusMd, "12px") | | Adjust typographic scale | css.Set(css.TextBase, "1.1rem") |


Design Tokens

Tokens are the single source of truth for all design decisions.

Group Purpose
Color — Brand Fixed identity colors (e.g. ColorPrimary, ColorOnPrimary)
Color — Theme Adaptive light/dark active layers (e.g. ColorBackground, ColorSurface)
Color — Pairs Coupled background/foreground surface decisions (e.g. SurfacePanel, SurfaceSunken)
Color — Computed Derived values via color-mix() (e.g. ColorSurfaceSunken, ColorSelection)
Interaction — Intensity Mix-percentage scale for Hover/Focus/Press derivations (MixHover, MixFocus, MixPress)
Typography — Size Font-size scale (Major Third ratio)
Typography — Extras Line-height, weight, letter-spacing
Spacing Margin/padding/gap scale (4px grid)
Border-radius Consistent corner rounding
Elevation Box-shadow scale
Motion Animation timing + easing curves
Z-index Stacking contract
Viewport classes Typed media-query enum (Mobile, Tablet, Desktop) for responsive layout
Breakpoints Viewport widths (BpSm/BpMd/BpLg/BpXl — container queries / JS only, not usable in @media)
Container widths Max-width primitives
Rail widths Sidebar/fixed-column scale (RailNarrow, RailWide)

Design Philosophy

  • Semantic names over valuesColorOnSurface not #ffffff. Names describe intent; values can change.
  • Contrast safety by type — Background/foreground design decisions are coupled in Pair structures. Contrast ratio tests guarantee compliance with WCAG >= 4.5:1.
  • Scales over magic numbers — Typography and spacing follow mathematical ratios so all values are proportional and limited.
  • Two-layer color pattern — Separates source values (per mode) from active tokens (used by components). @media (prefers-color-scheme) switches modes without JS.
  • Single override point — Apps only need to change source-layer or scale variables; the rest cascades automatically.

Documentation

  • AGENTS.md — Constraints for anyone (human or agent) changing this library: the WASM boundary, the no-duplicated-value rule, and the checklist for adding a token.
  • Technical Specifications — Detailed specifications of all active design tokens, browser-level light-dark handling, and the complete library contract.
  • Migration Guide — Upgrade paths between releases.
  • Theming Architecture — Detailed guide on RootCSS, single-winner slot, theme overrides, and type safety constraints.
  • Go-Typed CSS DSL Justification — Deep-dive on design rationale, technical constraints, and comparison with alternatives.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var (
	ColorPrimary   = Token{Name: "--color-primary", Dark: "#1b5d8c"}
	ColorOnPrimary = Token{Name: "--color-on-primary", Dark: "#FFFFFF"}
	ColorSuccess   = Token{Name: "--color-success", Dark: "#1e7a30"}
	ColorOnSuccess = Token{Name: "--color-on-success", Dark: "#FFFFFF"}
	ColorDanger    = Token{Name: "--color-danger", Dark: "#ba2c0d"}
	ColorOnDanger  = Token{Name: "--color-on-danger", Dark: "#FFFFFF"}

	ColorBackground   = Token{Name: "--color-background", Light: "#FFFFFF", Dark: "#0D1117"}
	ColorOnBackground = Token{Name: "--color-on-background", Light: "#1C1C1E", Dark: "#E6EDF3"}
	ColorSurface      = Token{Name: "--color-surface", Light: "#F2F2F7", Dark: "#161B22"}
	ColorOnSurface    = Token{Name: "--color-on-surface", Light: "#1C1C1E", Dark: "#E6EDF3"}
	ColorOutline      = Token{Name: "--color-outline", Light: "#D1D1D6", Dark: "#30363D"}
	ColorMuted        = Token{Name: "--color-muted", Light: "#6E6E73", Dark: "#8B949E"}

	ColorSurfaceSunken = Token{Name: "--color-surface-sunken", Dark: "color-mix(in oklab, var(--color-surface), var(--color-on-surface) 8%)"}
	ColorSelection     = Token{Name: "--color-selection", Dark: "color-mix(in oklab, var(--color-primary), transparent 85%)"}
	ColorOnSelection   = Token{Name: "--color-on-selection", Dark: "var(--color-on-surface)"}

	TextXs   = Token{Name: "--text-xs", Dark: "0.75rem"}
	TextSm   = Token{Name: "--text-sm", Dark: "0.875rem"}
	TextBase = Token{Name: "--text-base", Dark: "1rem"}
	TextLg   = Token{Name: "--text-lg", Dark: "1.25rem"}
	TextXl   = Token{Name: "--text-xl", Dark: "1.5rem"}
	Text2xl  = Token{Name: "--text-2xl", Dark: "2rem"}

	LeadingNormal     = Token{Name: "--leading-normal", Dark: "1.5"}
	FontWeightRegular = Token{Name: "--font-weight-regular", Dark: "400"}
	FontWeightMedium  = Token{Name: "--font-weight-medium", Dark: "500"}
	FontWeightBold    = Token{Name: "--font-weight-bold", Dark: "700"}

	Space0  = Token{Name: "--space-0", Dark: "0"}
	Space1  = Token{Name: "--space-1", Dark: "0.25rem"}
	Space2  = Token{Name: "--space-2", Dark: "0.5rem"}
	Space3  = Token{Name: "--space-3", Dark: "0.75rem"}
	Space4  = Token{Name: "--space-4", Dark: "1rem"}
	Space6  = Token{Name: "--space-6", Dark: "1.5rem"}
	Space8  = Token{Name: "--space-8", Dark: "2rem"}
	Space12 = Token{Name: "--space-12", Dark: "3rem"}

	RadiusSm   = Token{Name: "--radius-sm", Dark: "4px"}
	RadiusMd   = Token{Name: "--radius-md", Dark: "8px"}
	RadiusLg   = Token{Name: "--radius-lg", Dark: "16px"}
	RadiusFull = Token{Name: "--radius-full", Dark: "9999px"}

	ShadowSm = Token{Name: "--shadow-sm", Dark: "0 1px 2px rgba(0, 0, 0, 0.05)"}
	ShadowMd = Token{Name: "--shadow-md", Dark: "0 4px 6px rgba(0, 0, 0, 0.10)"}
	ShadowLg = Token{Name: "--shadow-lg", Dark: "0 10px 15px rgba(0, 0, 0, 0.10)"}

	DurationFast = Token{Name: "--duration-fast", Dark: "150ms"}
	DurationBase = Token{Name: "--duration-base", Dark: "250ms"}
	DurationSlow = Token{Name: "--duration-slow", Dark: "400ms"}
	EaseInOut    = Token{Name: "--ease-in-out", Dark: "cubic-bezier(0.4, 0,   0.2, 1)"}

	ZBase     = Token{Name: "--z-base", Dark: "0"}
	ZDropdown = Token{Name: "--z-dropdown", Dark: "100"}
	ZSticky   = Token{Name: "--z-sticky", Dark: "200"}
	ZModal    = Token{Name: "--z-modal", Dark: "300"}
	ZToast    = Token{Name: "--z-toast", Dark: "400"}
	ZTooltip  = Token{Name: "--z-tooltip", Dark: "500"}

	BpSm = Token{Name: "--bp-sm", Dark: "640px"}
	BpMd = Token{Name: "--bp-md", Dark: "768px"}
	BpLg = Token{Name: "--bp-lg", Dark: "1024px"}
	BpXl = Token{Name: "--bp-xl", Dark: "1280px"}

	MaxWReadable = Token{Name: "--max-w-readable", Dark: "65ch"}

	ColumnNarrow = Token{Name: "--column-narrow", Dark: "12rem"}
	ColumnMedium = Token{Name: "--column-medium", Dark: "20rem"}
	ColumnWide   = Token{Name: "--column-wide", Dark: "30rem"}

	// Rail widths — the fixed column of a Sidebar layout.
	RailNarrow = Token{Name: "--rail-narrow", Dark: "3.5rem"}
	RailWide   = Token{Name: "--rail-wide", Dark: "12rem"}

	MixHover = Token{Name: "--mix-hover", Dark: "15%"}
	MixFocus = Token{Name: "--mix-focus", Dark: "30%"}
	MixPress = Token{Name: "--mix-press", Dark: "45%"}
)
View Source
var (
	SurfacePrimary    = Pair{ColorPrimary, ColorOnPrimary}
	SurfacePanel      = Pair{ColorSurface, ColorOnSurface}
	SurfaceBackground = Pair{ColorBackground, ColorOnBackground}
	SurfaceSunken     = Pair{ColorSurfaceSunken, ColorOnSurface}
	SurfaceSelected   = Pair{ColorSelection, ColorOnSelection}
	SurfaceDanger     = Pair{ColorDanger, ColorOnDanger}
	SurfaceSuccess    = Pair{ColorSuccess, ColorOnSuccess}
)

Functions

func Focus added in v0.2.0

func Focus(t Token) string

func Hover added in v0.2.0

func Hover(t Token) string

Hover, Focus and Press return the standard interaction-state derivation for any base token: the base mixed toward the theme's contrasting extreme. The mixer is light-dark(black, white) so a hover darkens on a light theme and lightens on a dark one. The intensity is a token, so an app can retune it with Theme(Set(MixHover, "22%")) without republishing this package.

func Press added in v0.3.3

func Press(t Token) string

func Query added in v0.4.0

func Query(devices ...Device) string

Query joins several device classes into one condition list. Callers that mean "tablet and desktop" pass both rather than emitting two blocks. Duplicate and unknown values are dropped; the result is ordered Mobile, Tablet, Desktop regardless of argument order, so emission is deterministic.

func Raw added in v0.0.2

func Raw(css string) item

Types

type Device added in v0.4.0

type Device uint8

Device is the closed set of viewport classes. It exists because a media query condition cannot read a custom property: the pixel thresholds must be baked into the query string, and this is the only file in the ecosystem allowed to hold them.

The three classes are mutually exclusive and jointly exhaustive: every viewport width matches exactly one. That property is asserted by TestDeviceClassesPartition.

const (
	Mobile Device = iota
	Tablet
	Desktop
)

func (Device) Query added in v0.4.0

func (d Device) Query() string

Query returns the media-query condition for exactly this class, without the leading "@media ". Thresholds mirror BpSm (640px) and BpLg (1024px).

func (Device) String added in v0.4.0

func (d Device) String() string

type NamedPair added in v0.3.1

type NamedPair struct {
	Name string
	Bg   ValueGetter
	Fg   ValueGetter
	Min  float64
}

NamedPair is a descriptor used for contrast compliance verification.

func AllPairs added in v0.3.1

func AllPairs() []NamedPair

AllPairs returns the 7 functional design decision pairs for automated contrast auditing. SurfaceSunken and SurfaceSelected are excluded: their values are color-mix() expressions that resolveColor() cannot evaluate (known gap, documented in SPECS).

type Override added in v0.1.3

type Override struct {
	// contains filtered or unexported fields
}

Override is the customized override of a single Token's value.

func Set added in v0.1.3

func Set(t Token, value string) Override

Set builds an Override for a designated Token with the specified custom value.

func SetTheme added in v0.3.1

func SetTheme(t Token, light, dark string) Override

SetTheme builds an Override for a theme-aware Token with a custom light/dark pair.

type Pair added in v0.2.0

type Pair struct{ Bg, Fg Token }

Pair represents a complete surface decision: background and foreground colors coupled.

type RawItem added in v0.0.2

type RawItem string

type Stylesheet added in v0.0.2

type Stylesheet struct {
	// contains filtered or unexported fields
}

func NewStylesheet added in v0.1.0

func NewStylesheet(items ...item) *Stylesheet

func RenderCSS added in v0.0.2

func RenderCSS() *Stylesheet

RenderCSS is the base reset. Every rule sits in the `tokens` layer — the lowest of the four the widget style DSL declares. Unlayered, `svg { display: block }` would beat `@layer widgets { .part { display: none } }` regardless of specificity, so any component hiding an icon by state could not.

func RootCSS added in v0.0.2

func RootCSS() *Stylesheet

func Theme added in v0.1.3

func Theme(overrides ...Override) *Stylesheet

Theme returns the entire RootCSS() catalog with custom overrides appended.

func (*Stylesheet) String added in v0.0.2

func (s *Stylesheet) String() string

type Token added in v0.0.2

type Token struct {
	Name        string
	Light, Dark string // Dark solo = static fallback; ambos = light-dark pair
}

Token is a design token: a named visual decision with a fallback value. For static tokens, Dark holds the literal fallback and Light is empty. For theme tokens, Light and Dark hold the light/dark pair and Var/GetFallback generate light-dark(Light, Dark) automatically.

func (Token) GetFallback added in v0.3.1

func (t Token) GetFallback() string

GetFallback returns the default/fallback value of the token.

func (Token) GetName added in v0.3.1

func (t Token) GetName() string

GetName returns the CSS property name of the token.

func (Token) Var added in v0.0.2

func (t Token) Var() string

Var returns the CSS variable expression for the token, including its default fallback.

type ValueGetter added in v0.3.1

type ValueGetter interface {
	GetName() string
	GetFallback() string
}

ValueGetter defines the interface for retrieving token metadata.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL