Casca API Reference
Complete reference for all Casca components, CSS custom properties, and HTML patterns.
Table of Contents
- Bar Charts
- Pie & Donut Charts
- Line Charts
- Area Charts
- Progress Bars
- Gauge Charts
- Heatmap
- Scatter Plot
- Waterfall
- Radar
- Candlestick
- Axis Labels
- UI Components
- Interactions (no-JS)
- Layout Shell (extended-layout)
- Layout Primitives
- CSS Custom Properties
- SSG Markdown Output
- CLI Theme Tools
- CLI Lint Checks
SSG Markdown Output
casca site emits Casca-classed markdown HTML by default. Rendered tables get
casca-data, casca-data-head, and casca-data-body; fenced code blocks get
casca-code and, when present, data-language; strong-leading blockquotes are
wrapped in <aside class="casca-prose-callout" data-callout="note" aria-label="Note">. The native <aside> element provides the complementary
landmark and the aria-label carries the accessible name.
Use [markdown].casca_primitive_mapping = false or the
[markdown.opt_out] flags for compatibility with older markdown output. Use
[markdown].wrap_in_casca_layout = false to disable automatic
<casca-layout> wrapping. Route-level [[layouts]].variant values override
[markdown].default_variant for wrapped markdown pages.
Markdown Authoring
Markdown pages can invoke any Casca primitive through three authoring modes:
Mode 1: Attribute-driven inline elements. Use HTML custom elements with attributes to embed controls directly in markdown:
<casca-progress value="65" max="100" label="Development"></casca-progress>
<casca-gauge value="72" max="100" label="CPU" thresholds></casca-gauge>
<casca-range min="0" max="100" value="50" label="Volume"></casca-range>
<casca-date label="Due date"></casca-date>
<casca-color label="Accent" value="#3b82f6"></casca-color>
<casca-toggles>
<casca-toggle value="1" label="Day" checked></casca-toggle>
<casca-toggle value="2" label="Week"></casca-toggle>
</casca-toggles>
<casca-divider-pixel label="Section"></casca-divider-pixel>
<casca-theme-image src="/img/logo.png" width="48" height="48"></casca-theme-image>
Mode 2: Slot-driven inline elements. Use <template slot="..."> children
to fill named slots in layout primitives. Extra attributes on the <template>
tag carry metadata (Pattern A):
<casca-card>
<template slot="title">Getting Started</template>
<template slot="body"><p>Content here.</p></template>
<template slot="footer">Last updated: June 2026</template>
</casca-card>
<casca-stat>
<template slot="label">Revenue</template>
<template slot="value">$48.2k</template>
<!-- trend is an attribute on the delta template, not a separate slot -->
<template slot="delta" trend="up">+12.4%</template>
<template slot="caption">vs last month</template>
</casca-stat>
<casca-switch>
<template slot="controls">
<casca-switch-btn value="1" label="Day" checked></casca-switch-btn>
<casca-switch-btn value="2" label="Week"></casca-switch-btn>
</template>
<!-- each logical view is its own template -->
<template slot="view" view="1"><p>Day view content</p></template>
<template slot="view" view="2"><p>Week view content</p></template>
</casca-switch>
Body content (non-template children) fills the default slot:
<casca-filter>
<template slot="controls">
<casca-filter-chip value="1" label="Cat A" checked></casca-filter-chip>
<casca-filter-chip value="2" label="Cat B" checked></casca-filter-chip>
</template>
<!-- items are plain children, not a template slot -->
<li data-filter="1">Item one</li>
<li data-filter="2">Item two</li>
</casca-filter>
<casca-segment checked="">
<template slot="label-off">Core</template>
<template slot="label-on">Extended</template>
<!-- default content is plain children -->
<div class="casca-card-grid">
<div class="casca-card">Always shown</div>
<div class="casca-card casca-segment-extra">Toggle shown</div>
</div>
</casca-segment>
Mode 3: Chart primitives with TOML DSL. Data-heavy chart primitives use
fenced code blocks with a casca-* language tag. The body is TOML:
```casca-bar
title = "Monthly Sales"
labels = ["Jan", "Feb", "Mar"]
[[series]]
label = "Revenue"
values = [65, 80, 45]
```
Supported chart fenced blocks: casca-bar, casca-line, casca-area,
casca-pie, casca-heatmap, casca-scatter, casca-waterfall,
casca-radar, casca-candlestick.
Chart primitives auto-synthesize accessible .casca-data tables from the
markdown-authored data alongside the visual chart.
Obsidian Callout Syntax
Blockquotes starting with [!TYPE] are detected and mapped to Casca prose
callouts with appropriate ARIA roles and labels:
> [!note] Optional Title
> Callout body content.
The [!TYPE] marker is stripped from visible output. The text following the
marker on the first line becomes the callout title.
Supported types: note, warning, tip, danger, info, example,
question, abstract, success, failure, bug, todo, quote.
The existing > **Type:** Body syntax continues to work unchanged.
Strict Mode
When [site].strict = true, unknown <casca-*> elements produce a build
error. When strict mode is off (strict = false, the default), unknown
primitives emit a warning to stderr but do not block the build. Use
[markdown.opt_out].primitives to selectively disable specific primitives
without triggering strict-mode errors or warnings:
Frontmatter metadata
Markdown pages may carry YAML or TOML frontmatter for per-page metadata overrides. See the Frontmatter section in the SSG guide for the full key reference, type constraints, and variant resolution order.
Bar Charts
Basic Bar Chart
<figure class="casca casca-figure">
<figcaption class="casca-title">Chart Title</figcaption>
<table class="casca-data">
<caption>Chart Title</caption>
<thead><tr><th scope="col">Label</th><th scope="col">Value</th></tr></thead>
<tbody><tr><th scope="row">Label</th><td>75</td></tr></tbody>
</table>
<div class="casca-bar" aria-hidden="true">
<div class="casca-bar-group">
<div class="casca-bar-item">
<div class="casca-bar-value" style="--value: 75%; --color: var(--casca-color-1)"></div>
</div>
</div>
<div class="casca-bar-labels">
<span class="casca-bar-label">Label</span>
</div>
</div>
</figure>
Diverging Baseline Bars (Cash Flow)
NEW in v0.2.0
<div class="casca-bar" data-diverging aria-hidden="true">
<div class="casca-bar-group" data-grid>
<div class="casca-bar-item">
<!-- Positive value (above baseline) -->
<div class="casca-bar-value"
data-direction="positive"
style="--value: 30%; --color: var(--casca-color-5)"
data-label="+$15k"></div>
</div>
<div class="casca-bar-item">
<!-- Negative value (below baseline) -->
<div class="casca-bar-value"
data-direction="negative"
style="--value: 16%; --color: var(--casca-color-8)"
data-label="-$8k"></div>
</div>
</div>
</div>
Attributes:
data-diverging- Enables diverging baseline modedata-direction="positive|negative"- Bar direction from center
Grouped Bars
NEW in v0.2.0
<div class="casca-bar" aria-hidden="true">
<div class="casca-bar-group">
<div class="casca-bar-item">
<div class="casca-bar-grouped">
<div class="casca-bar-value" style="--value: 65%; --color: var(--casca-color-1)"></div>
<div class="casca-bar-value" style="--value: 75%; --color: var(--casca-color-2)"></div>
<div class="casca-bar-value" style="--value: 85%; --color: var(--casca-color-3)"></div>
</div>
</div>
</div>
</div>
Stacked Bars
<div class="casca-bar" aria-hidden="true">
<div class="casca-bar-group">
<div class="casca-bar-item">
<div class="casca-bar-stack">
<div class="casca-bar-segment" style="--value: 25%; --color: var(--casca-color-1)" data-label="Segment 1"></div>
<div class="casca-bar-segment" style="--value: 15%; --color: var(--casca-color-2)" data-label="Segment 2"></div>
<div class="casca-bar-segment" style="--value: 10%; --color: var(--casca-color-3)" data-label="Segment 3"></div>
</div>
</div>
</div>
</div>
Horizontal Bars
<div class="casca-bar" data-orientation="horizontal" aria-hidden="true">
<!-- Same structure as vertical bars -->
</div>
Bar Chart Attributes
| Attribute | Values | Description |
|---|---|---|
data-grid | - | Show background grid lines |
data-diverging | - | Enable diverging baseline mode |
data-orientation | horizontal | Horizontal bar layout |
data-direction | positive, negative | Bar direction (diverging only) |
data-label | String | Value label text (on the bar) |
data-labels | always | Show data-label values without hover (touch / print / static no-JS); default is hover-only |
Bar Chart CSS Variables
| Variable | Default | Description |
|---|---|---|
--value | 0% | Bar height/length (0-100%) |
--color | var(--casca-color-1) | Bar fill color |
--casca-bar-width | 2rem | Bar width |
--casca-bar-radius | 0.25rem | Bar border radius |
--casca-bar-min-height | 2px | Minimum bar height |
--casca-bar-rank-label-width | 7rem | Leading label column in the ranked list |
Always-visible values & ranked lists
By default a bar's data-label shows on hover (a dark pill). Add
data-labels="always" to the .casca-bar to render the values as plain
end-of-bar text with no hover or pointer dependency - so the numbers are
visible on touch, in print, in a screenshot, and on a static server-rendered
no-JS page. The chart reserves headroom (vertical) / trailing room (horizontal)
so labels are not clipped. The default (no attribute) is unchanged.
For the analytics "top N" view, compose horizontal orientation + a leading
.casca-bar-label and a trailing .casca-bar-rank-value (real text) per row.
Each .casca-bar-item then lays out as label · bar · value, sharing one
baseline, and reads "label … value" in DOM order even with the stylesheet absent:
<div class="casca-bar" data-orientation="horizontal" data-labels="always"
style="--casca-height: auto" aria-hidden="true">
<div class="casca-bar-group">
<div class="casca-bar-item">
<span class="casca-bar-label">Organic search</span>
<div class="casca-bar-value" style="--value: 100%"></div>
<span class="casca-bar-rank-value">4,200</span>
</div>
<!-- …one row per item, sorted descending… -->
</div>
</div>
The visual chart stays aria-hidden; the .casca-data table remains the
source of truth for assistive tech. Meaning is carried by the value text, never
color alone.
Pie & Donut Charts
Pie Chart
<div class="casca-pie" aria-hidden="true">
<div class="casca-pie-chart"
data-segments="4"
style="
--angle-1: 90deg;
--angle-2: 180deg;
--angle-3: 270deg;
--color-1: var(--casca-color-1);
--color-2: var(--casca-color-2);
--color-3: var(--casca-color-3);
--color-4: var(--casca-color-4);
">
</div>
</div>
Pair the visual with a sibling <table class="casca-data"> inside the same
<figure class="casca casca-figure"> so screen readers receive the exact
values, as in the heatmap example below.
Donut Chart
<div class="casca-pie" aria-hidden="true">
<div class="casca-pie-chart"
data-variant="donut"
data-segments="3"
style="--angle-1: 120deg; --angle-2: 240deg;">
<!-- Optional center label -->
<div class="casca-pie-center">
75%
<span class="casca-pie-center-label">Complete</span>
</div>
</div>
</div>
Pie Chart Attributes
| Attribute | Values | Description |
|---|---|---|
data-segments | 0-8 | Number of pie segments. 1 renders a solid single-category disc (--color-1); 0 (or omitted) renders a neutral "no data" disc |
data-variant | donut | Donut chart with center hole |
Pie Chart CSS Variables
| Variable | Default | Description |
|---|---|---|
--angle-N | 0deg | Cumulative angle for segment N (1-8) |
--color-N | var(--casca-color-N) | Color for segment N |
--casca-pie-size | 250px | Pie diameter |
--casca-donut-hole-size | 40% | Donut hole size (percentage) |
Heatmap
NEW in v0.3.0
A read-only, CSS-only color-intensity grid: --casca-heatmap-cols columns of
square cells whose fill blends --casca-heatmap-empty → --casca-heatmap-color
by each cell's --value (0–1). Natural fit for calendar / availability overviews
and correlation matrices. It pairs above the form-aware Slot Grid
as the read-only overview (same day-column model).
This is a visual-only chart: mark the grid aria-hidden and pair it with a
.casca-data table (like the bar/pie charts). Add a native title="…" per cell
for an exact-value, no-JS tooltip.
<figure class="casca casca-figure">
<figcaption class="casca-title">Typical free time by day</figcaption>
<table class="casca-data"><!-- exact values for screen readers --></table>
<div class="casca-heatmap" aria-hidden="true" style="--casca-heatmap-cols: 7">
<span class="casca-heatmap-colhead">Mon</span>
<!-- … 6 more headers … -->
<div class="casca-heatmap-cell" style="--value: 0.8" title="Mon - 80% free"></div>
<!-- … more cells, row-major … -->
</div>
<!-- optional intensity legend -->
<div class="casca-heatmap-scale" aria-hidden="true">
<span>Busy</span><span class="casca-heatmap-scale-bar"></span><span>Free</span>
</div>
</figure>
Classes:
| Class | Description |
|---|---|
.casca-heatmap | The grid container (--casca-heatmap-cols equal columns) |
.casca-heatmap-colhead | Optional column header (sits in the grid's first row) |
.casca-heatmap-cell | A cell; --value (0–1) drives fill intensity |
.casca-heatmap-scale | Optional legend row (Busy → Free) |
.casca-heatmap-scale-bar | The gradient bar inside the legend |
Heatmap CSS Variables:
| Variable | Default | Description |
|---|---|---|
--casca-heatmap-cols | 7 | Number of columns (set inline per grid) |
--value | 0 | Per-cell intensity, 0–1 (on .casca-heatmap-cell) |
--casca-heatmap-color | var(--casca-color-1) | Full-intensity (value 1) color |
--casca-heatmap-empty | var(--casca-gray-2) | Empty (value 0) color |
--casca-heatmap-empty-dark | var(--casca-gray-8) | Empty color in dark mode |
--casca-heatmap-gap | var(--casca-size-1) | Gap between cells |
--casca-heatmap-radius | var(--casca-radius-1) | Cell corner radius |
Features:
- ✅ Fill via
color-mix()(empty → color by--value), with an@supportsopacity fallback for engines without it - ✅ Dark-mode aware; works from the core build
- ✅ Forced-colors safe (degrades to bordered cells; values stay in the data table)
- ✅ Square cells via
aspect-ratio, responsive through grid1frcolumns
See site/pages/charts/heatmap.html.
Scatter Plot
NEW in v0.4.0
A read-only, CSS-only scatter / bubble plot. Each point is absolutely positioned
over a plot box by its --x / --y (both 0–100, percent of the axis range -
the server normalizes raw values, keeping this no-JS). --size makes bubbles;
--color encodes categories.
This is a visual-only chart: mark the plot aria-hidden and pair it with a
.casca-data table listing (x, y[, category]). Add a native title="…" per
point for a no-JS value tooltip.
<figure class="casca casca-figure">
<figcaption class="casca-title">Study hours vs. exam score</figcaption>
<table class="casca-data"><!-- (x, y) per point for screen readers --></table>
<div class="casca-scatter" aria-hidden="true" data-grid>
<div class="casca-scatter-point" style="--x: 20; --y: 45" title="2h → 45"></div>
<!-- bubble + category -->
<div class="casca-scatter-point"
style="--x: 80; --y: 88; --size: 1.25rem; --color: var(--casca-color-5)"></div>
</div>
</figure>
Classes / attributes:
| Class / attr | Description |
|---|---|
.casca-scatter | The plot box (position: relative, aspect-ratio, left+bottom axis frame) |
data-grid | Opt-in graph-paper grid background |
.casca-scatter-point | A point; positioned by --x / --y (0–100) |
.casca-scatter-trend | Opt-in trend line; full-width band from --y1 (left edge) to --y2 (right edge) |
.casca-scatter-quadrants | Opt-in dividers; vertical rule at --qx, horizontal at --qy |
.casca-scatter-quadrant-label | A quadrant corner label; positioned by --x / --y |
Scatter CSS Variables:
| Variable | Default | Description |
|---|---|---|
--x / --y | 0 | Point / label position, 0–100 (percent of plot) |
--size | --casca-scatter-point-size | Point diameter (bubble variant) |
--color | var(--casca-color-1) | Point color (category) |
--y1 / --y2 | 50 | Trend line y at the left / right edge, 0–100 (server-fit) |
--qx / --qy | 50 | Quadrant divider positions, 0–100 |
--casca-scatter-aspect | 4 / 3 | Plot aspect ratio |
--casca-scatter-point-size | 0.75rem | Default point diameter |
--casca-scatter-grid-color | var(--casca-grid-color) | Grid line color |
--casca-scatter-grid-step | 25% | Grid spacing |
--casca-scatter-trend-color | var(--casca-label-color) | Trend line color |
--casca-scatter-trend-width | 1.5 | Trend band thickness (% of plot height) |
--casca-scatter-quadrant-color | var(--casca-axis-color) | Quadrant divider color |
Features:
- ✅ Absolute
%positioning - O(points) DOM, naturally responsive, dark-mode aware - ✅ Bubble (
--size) and category (--color) variants - ✅ Optional trend line (
clip-pathband - server passes the fit, no in-browser regression) and quadrant dividers for 2×2 analysis - ✅ Forced-colors safe (points → outlined dots, trend → system color, dividers use borders; values stay in the table)
- ✅ Reduced-motion safe (hover scale handled by the core reduced-motion reset)
Axis tick labels are a shared primitive - see Axis Labels. The trend line spans the full x-range (the regression line); state its meaning (e.g. "positive correlation") in text, since the overlays are visual aids and the
.casca-datatable is the data source.
<!-- trend line + quadrant dividers (both opt-in children of the plot) -->
<div class="casca-scatter" aria-hidden="true">
<div class="casca-scatter-trend" style="--y1: 24; --y2: 96"></div>
<div class="casca-scatter-quadrants" style="--qx: 50; --qy: 50">
<span class="casca-scatter-quadrant-label" style="--x: 25; --y: 90">Quick wins</span>
</div>
<!-- … points … -->
</div>
See site/pages/charts/scatter.html.
Waterfall
NEW in v0.5.0
A read-only, CSS-only waterfall: floating bars showing the cumulative effect of
sequential increases / decreases, with running-total bars at the ends. Built for
cash flow and budget analysis. Each bar carries --start / --end (0–100, the
cumulative level before/after the step, as a percent of the value range - the
server computes the running total). The segment floats between those levels
(derived with min()/max(), so no per-bar base/height math); a dashed
connector bridges each gap at the --end level. data-direction picks the color.
Visual-only: mark the chart aria-hidden and pair it with a .casca-data table.
<figure class="casca casca-figure">
<figcaption class="casca-title">Q1 cash flow</figcaption>
<table class="casca-data"><!-- start, each delta, end --></table>
<div class="casca-waterfall" aria-hidden="true" data-grid>
<div class="casca-waterfall-bar" data-direction="total" style="--start: 0; --end: 50">
<span class="casca-waterfall-cap">$50k</span>
</div>
<div class="casca-waterfall-bar" data-direction="increase" style="--start: 50; --end: 85">
<span class="casca-waterfall-cap">+$35k</span>
</div>
<div class="casca-waterfall-bar" data-direction="decrease" style="--start: 85; --end: 65">
<span class="casca-waterfall-cap">−$20k</span>
</div>
<div class="casca-waterfall-bar" data-direction="total" style="--start: 0; --end: 65">
<span class="casca-waterfall-cap">$65k</span>
</div>
</div>
<div class="casca-waterfall-labels">
<span class="casca-waterfall-label">Start</span>
<!-- … one per bar … -->
</div>
</figure>
Classes / attributes:
| Class / attr | Description |
|---|---|
.casca-waterfall | The chart container (flex row of bars, baseline at 0) |
data-grid | Opt-in horizontal gridlines |
.casca-waterfall-bar | One step; --start / --end (0–100) set the floating segment |
data-direction | increase | decrease | total - picks the segment color |
.casca-waterfall-cap | Optional value label floated above the segment |
.casca-waterfall-labels / -label | X-axis category label row |
Waterfall CSS Variables:
| Variable | Default | Description |
|---|---|---|
--start / --end | 0 | Cumulative level before / after the step (0–100; on the bar) |
--casca-waterfall-height | 240px | Plot height |
--casca-waterfall-increase | var(--casca-color-5) | Increase color |
--casca-waterfall-decrease | var(--casca-color-8) | Decrease color |
--casca-waterfall-total | var(--casca-color-1) | Total-bar color |
--casca-waterfall-gap | var(--casca-size-3) | Gap between bars |
--casca-waterfall-connector-color | var(--casca-axis-color) | Connector line color |
Features:
- ✅
min()/max()-derived segments - server only computes the running total - ✅ Dashed connectors auto-aligned at each step's end level
- ✅ Dark-mode aware (colors via tokens); forced-colors safe
- ✅ Works from the core build
See site/pages/charts/waterfall.html.
Radar
NEW in v0.6.0
A read-only, CSS-only radar (spider) chart: one filled polygon per data series
over a circular gridline backdrop. Each series is positioned by --points - the
comma-separated x% y% vertices (one per axis) that the server computes from
polar coordinates (center 50% 50%; for axis i at angle θ and value v in
0–1, the vertex is 50% + v*50%*cosθ, 50% + v*50%*sinθ). CSS clips the series
box to that polygon - no in-browser trig. Translucent series overlap to compare
profiles (skills, products, reviews).
Visual-only: mark it aria-hidden, pair with a .casca-data table. Spoke
labels use the shared Axis Labels primitive
(data-axis="radial") - see the example.
<figure class="casca casca-figure">
<figcaption class="casca-title">Player comparison</figcaption>
<table class="casca-data"><!-- per-axis values --></table>
<div class="casca-radar" style="--casca-radar-axes: 6" aria-hidden="true">
<div class="casca-radar-series"
style="--points: 50% 5%, 76% 35%, 80% 68%, 50% 75%, 15% 70%, 24% 35%;
--color: var(--casca-color-1)"></div>
<!-- … more series … -->
</div>
</figure>
Classes / variables:
| Class / var | Description |
|---|---|
.casca-radar | The square plot with circular grid (set --casca-radar-axes = spoke count) |
.casca-radar-series | One series; --points (server-computed vertices) + --color |
--casca-radar-axes | 6 |
--casca-radar-grid-color | var(--casca-grid-color) |
--casca-radar-fill-opacity | 0.35 |
Features:
- ✅
clip-path: polygon(var(--points))fill - server does the polar math, no JS - ✅ Any axis count via
--casca-radar-axes; multiple series overlap - ✅ Dark-mode aware (colors via tokens)
Circular grid (rings + spokes). Polygon-ring (spider-web) grids and vertex dots are deferred follow-ons. Spoke labels now use the shared Axis Labels primitive. Forced-colors strips the fills - the
.casca-datatable carries the values.
See site/pages/charts/radar.html.
Candlestick
NEW in v0.7.0
A read-only, CSS-only OHLC candlestick chart. Each candle carries --high,
--low, --open, --close (0–100, a percent of the price range - the server
normalizes the prices). A thin wick spans low→high; a wider body spans
open↔close (derived with min()/max(), with a doji floor so open==close still
shows a line). data-direction colors it up (close ≥ open) or down.
Visual-only: mark it aria-hidden, pair with a .casca-data table (date + OHLC).
<figure class="casca casca-figure">
<figcaption class="casca-title">ACME - daily</figcaption>
<table class="casca-data"><!-- date, O, H, L, C --></table>
<div class="casca-candlestick" aria-hidden="true">
<div class="casca-candle" data-direction="up"
style="--low: 38; --high: 56; --open: 40; --close: 52"></div>
<div class="casca-candle" data-direction="down"
style="--low: 48; --high: 64; --open: 60; --close: 50"></div>
<!-- … more candles … -->
</div>
</figure>
Classes / variables:
| Class / var | Description |
|---|---|
.casca-candlestick | The chart container (flex row of candles) |
.casca-candle | One candle; --open / --high / --low / --close (0–100) |
data-direction | up (default) | down - picks the candle color |
--casca-candle-up | var(--casca-color-5) |
--casca-candle-down | var(--casca-color-8) |
--casca-candlestick-height | 240px |
--casca-candle-body-inset | 22% |
Features:
- ✅
min()/max()-derived body + amax(…, --casca-candle-min-body)doji floor - ✅ Server passes raw normalized OHLC - no per-candle base/height math, no JS
- ✅ Dark-mode aware (colors via tokens); forced-colors safe
See site/pages/charts/candlestick.html.
Axis Labels
NEW in v1.1.0
A shared, server-fed primitive for axis labels so charts don't reinvent them. Two geometries cover the catalog, both driven entirely by custom properties (no JS):
- Linear ticks - a label band along an x or y edge; each tick sits at
--pos(0–100, percent of the axis), mirroring the scatter point's--x/--ymodel. For scatter, line, area, and numeric bar axes. - Radial labels - one label per spoke around a circle, placed from a 0-based
--indexand the spoke count, using CSScos()/sin(). For radar.
An optional .casca-plot grid frame seats a y-axis band left of the plot and an
x-axis band below (plus optional titles), so the whole figure is composed with
classes and no page-local CSS.
This primitive is additive: the per-chart evenly-distributed labels
(.casca-bar-labels, .casca-line-labels, .casca-area-labels) keep
working unchanged - those remain the categorical convenience; .casca-axis is
the general positioned / radial primitive.
<!-- Linear: a framed scatter with y + x tick bands and titles -->
<div class="casca-plot">
<span class="casca-axis-title" data-axis="y">Exam score</span>
<div class="casca-axis" data-axis="y">
<span class="casca-axis-tick" style="--pos: 0">0</span>
<span class="casca-axis-tick" style="--pos: 50">50</span>
<span class="casca-axis-tick" style="--pos: 100">100</span>
</div>
<div class="casca-scatter" aria-hidden="true" data-grid><!-- points --></div>
<div class="casca-axis" data-axis="x">
<span class="casca-axis-tick" style="--pos: 0">0h</span>
<span class="casca-axis-tick" style="--pos: 50">5h</span>
<span class="casca-axis-tick" style="--pos: 100">10h</span>
</div>
<span class="casca-axis-title" data-axis="x">Study hours</span>
</div>
<!-- Radial: the band is a SIBLING of the aria-hidden radar, in a positioned box -->
<div style="position: relative">
<div class="casca-radar" style="--casca-radar-axes: 6" aria-hidden="true"><!-- series --></div>
<div class="casca-axis" data-axis="radial" style="--casca-axis-count: 6">
<span class="casca-axis-tick" style="--index: 0">Speed</span>
<span class="casca-axis-tick" style="--index: 1">Power</span>
<!-- …one tick per spoke; angle = index/count*360deg − 90deg (0 = top) -->
</div>
</div>
Classes / attributes:
| Class / attr | Description |
|---|---|
.casca-plot | Optional grid frame; seats y-band + x-band + titles around the plot |
.casca-axis + data-axis="x" | Horizontal tick band; ticks positioned by --pos from the inline start |
.casca-axis + data-axis="y" | Vertical tick band; ticks positioned by --pos from the bottom |
.casca-axis + data-axis="radial" | Spoke-label band (sibling of the plot); ticks positioned by --index |
.casca-axis-tick | One label; --pos (linear) or --index (radial) |
.casca-axis-title + data-axis="x|y" | Axis caption; y reads bottom-to-top |
CSS Variables:
| Variable | Default | Description |
|---|---|---|
--pos | 0 | Linear tick position, 0–100 (percent of the axis; set on the tick) |
--index | 0 | Radial tick index, 0-based (set on the tick) |
--casca-axis-count | 6 | Radial spoke count (set on the band to match the radar) |
--casca-axis-radius | 56% | Radial label distance from center (just outside the ring) |
--casca-axis-y-width | 2.25rem | Left tick gutter in .casca-plot |
--casca-axis-x-height | 1.25rem | Bottom tick gutter in .casca-plot |
Features:
- ✅ Labels are real DOM text - screen-reader legible; the
.casca-datatable stays the source of truth - ✅ Linear ticks use only
calc()+translate(universal); radial usescos()/sin()with a flow fallback - ✅ Dark-mode aware (color via
--casca-label-color); forced-colors safe (text) - ✅ No color-only meaning
See site/pages/charts/scatter.html and site/pages/charts/radar.html.
Prose
casca-prose
Wraps long-form content (rendered Markdown, docs, blog posts) in an editorial column with measured width, comfortable leading, link affordances, and code formatting. Designed for the output shape of a CommonMark / GFM pipeline. No per-element class names are required on the content; the primitive targets the bare elements the pipeline emits.
<article class="casca-prose">
<h1>Page title</h1>
<p>A paragraph of body copy with <a href="...">an inline link</a> and
<code>inline code</code>.</p>
<h2>Section heading</h2>
<ul>
<li>Lists hang their markers outside the column.</li>
<li>Marker color follows <code>--casca-color-1</code> by default.</li>
</ul>
<pre><code>// code blocks get their own dark background</code></pre>
<blockquote>
<p>Blockquotes get an accent stripe and italic body.</p>
</blockquote>
<hr>
<p>Horizontal rules render as a thin colored line.</p>
</article>
casca-prose CSS Variables
Override at :root or directly on the .casca-prose element. All have
sensible defaults that consume Casca's core token surface, so the primitive
works out of the box.
| Variable | Default | Purpose |
|---|---|---|
--casca-prose-max-width | 45rem (720px) | Measured column width |
--casca-prose-font-family | 'Newsreader', 'Iowan Old Style', Georgia, serif | Body font stack |
--casca-prose-font-size | 1.0625rem (17px) | Body font size |
--casca-prose-line-height | 1.6 | Body leading |
--casca-prose-color | var(--casca-gray-9) | Body color (light) |
--casca-prose-color-dark | var(--casca-gray-1) | Body color (dark) |
--casca-prose-heading-color | var(--casca-gray-9) | Heading color (light) |
--casca-prose-heading-color-dark | var(--casca-gray-0) | Heading color (dark) |
--casca-prose-heading-font | var(--casca-font-sans) | Heading font stack |
--casca-prose-link-color | var(--casca-color-1) | Link color |
--casca-prose-link-color-hover | var(--casca-color-2) | Link hover color |
--casca-prose-code-color | var(--casca-gray-9) | Inline code color |
--casca-prose-code-bg | var(--casca-gray-2) | Inline code background |
--casca-prose-code-font | var(--casca-font-mono) | Code font stack |
--casca-prose-pre-color | var(--casca-gray-0) | Block code color |
--casca-prose-pre-bg | var(--casca-gray-9) | Block code background |
--casca-prose-blockquote-color | var(--casca-gray-7) | Blockquote text |
--casca-prose-blockquote-border | var(--casca-color-1) | Blockquote accent stripe |
--casca-prose-rule-color | var(--casca-gray-3) | <hr> and table rules |
--casca-prose-marker-color | var(--casca-color-1) | List marker accent |
--casca-prose-block-spacing | var(--casca-size-4) | Vertical rhythm |
Contract notes
- All selectors are descendants of
.casca-prose. No host-page effect. - Dark-mode variants live behind
@media (prefers-color-scheme: dark)and consume*-darktokens with the same fallback chain as their light counterparts. - The body font defaults to Newsreader and falls back to Iowan Old Style →
Georgia → serif. Wire the Newsreader webfont via
@font-faceor override--casca-prose-font-familyif a different serif is wanted. - The primitive does NOT bind to mood overlays directly. Themes that wish to recolor prose override the prose tokens at the same scope they override the rest of Casca.
Prose Callouts
Callouts render advisory content inside .casca-prose. The canonical form is
explicit HTML:
<aside class="casca-prose-callout" data-callout="warning" aria-label="Warning">
<p><strong>Heads up.</strong> Body text that explains the warning.</p>
</aside>
data-callout accepts note, tip, warning, danger, or important.
Omitting data-callout, using data-callout="note", or using an unknown value
renders the note treatment. The explicit form should carry an aria-label
matching the intent (Note, Tip, Warning, Danger, or Important) unless
the host needs localized label text. Native <aside> already exposes the
complementary landmark, so no role attribute is required.
Casca also elevates Markdown-style note blockquotes:
<blockquote>
<p><strong>Note.</strong> A bold-leading blockquote renders as a note callout.</p>
</blockquote>
The auto-detect selector is
blockquote:has(> p:first-child > strong:first-child). It always maps to the
note variant. To opt out for a real quotation that starts with bold text, wrap
the bold run so it is not the first child of the first paragraph.
<blockquote>
<p><span><strong>Hello,</strong></span> she said.</p>
</blockquote>
Prose Disclosure
Native <details> and <summary> elements are styled inside .casca-prose
without classes:
<details>
<summary>Advanced configuration</summary>
<p>Body content revealed when the disclosure is open.</p>
</details>
The native keyboard and expanded-state semantics remain intact. The UA marker
is suppressed with summary { list-style: none; } plus
summary::-webkit-details-marker { display: none; }; Casca draws a CSS marker
that rotates when the disclosure is open.
Prose Definition Lists
Definition lists use plain semantic HTML:
<dl>
<dt><code>--casca-prose-max-width</code></dt>
<dd>Measured prose column width.</dd>
<dt><code>--casca-prose-anchor-content</code></dt>
<dd>Visible heading-anchor glyph emitted by CSS.</dd>
</dl>
<dt> uses the prose heading face and weight. <dd> stays in body text and
uses a stable inline indent.
Prose Heading Anchors
When the SSG anchor-headings widget is active, casca site emits a child link
as the last child of each heading inside a .casca-prose ancestor that has an
id. Headings outside .casca-prose keep id auto-injection, the widget's
pre-1.5.0 behavior, but do not receive a permalink anchor:
<h2 id="callouts">
Callouts
<a class="casca-prose-anchor" href="#callouts"><span class="casca-prose-anchor-label">Link to section</span></a>
</h2>
The visible glyph is not in the HTML. CSS emits it with
.casca-prose-anchor::after { content: var(--_anchor-content); }, defaulting
to #. The .casca-prose-anchor-label span is real text and supplies the
accessible name; no aria-label is required on the link.
The SSG emission is idempotent. A heading that already contains a direct child
<a class="casca-prose-anchor"> is preserved without a duplicate anchor.
Prose Extension Selector Summary
| Selector / attribute | Purpose |
|---|---|
.casca-prose .casca-prose-callout | Explicit note callout, also fallback for missing or unknown data-callout |
.casca-prose .casca-prose-callout[data-callout="tip"] | Tip callout role color |
.casca-prose .casca-prose-callout[data-callout="warning"] | Warning callout role color |
.casca-prose .casca-prose-callout[data-callout="danger"] | Danger callout role color |
.casca-prose .casca-prose-callout[data-callout="important"] | Important callout role color |
.casca-prose blockquote:has(> p:first-child > strong:first-child) | Auto-detected note callout |
.casca-prose details, .casca-prose summary | Native prose disclosure styling |
.casca-prose dl, .casca-prose dt, .casca-prose dd | Definition-list typography |
.casca-prose .casca-prose-anchor | SSG-emitted heading permalink |
.casca-prose .casca-prose-anchor-label | Visually-hidden heading-anchor accessible name |
Prose Extension CSS Variables
Override at :root or on .casca-prose.
| Variable | Default | Purpose |
|---|---|---|
--casca-prose-callout-padding-block | var(--casca-size-3) | Callout block padding |
--casca-prose-callout-padding-inline | var(--casca-size-4) | Callout inline padding |
--casca-prose-callout-radius | var(--casca-radius-2) | Callout corner radius |
--casca-prose-callout-border-width | 1px | Callout top, end, and bottom border width |
--casca-prose-callout-accent-width | 3px | Callout leading accent stripe |
--casca-prose-callout-color | per variant | Optional override for the active callout role color |
--casca-prose-callout-bg | page background chain, tinted with color-mix() when supported | Callout background |
--casca-prose-callout-border | var(--casca-rule, var(--casca-gray-3)), tinted with color-mix() when supported | Callout border color |
--casca-prose-callout-note / --casca-prose-callout-note-dark | var(--casca-gray-7) / var(--casca-gray-3) | Note role color |
--casca-prose-callout-tip / --casca-prose-callout-tip-dark | var(--casca-color-2) | Tip role color |
--casca-prose-callout-warning / --casca-prose-callout-warning-dark | var(--casca-color-3) | Warning role color |
--casca-prose-callout-danger / --casca-prose-callout-danger-dark | var(--casca-color-8) | Danger role color |
--casca-prose-callout-important / --casca-prose-callout-important-dark | var(--casca-color-1) | Important role color |
--casca-prose-details-bg / --casca-prose-details-bg-dark | transparent | Disclosure background |
--casca-prose-details-border / --casca-prose-details-border-dark | var(--casca-gray-3) / var(--casca-gray-7) | Disclosure border |
--casca-prose-details-border-width | 1px | Disclosure border width |
--casca-prose-details-padding-block | var(--casca-size-3) | Disclosure block padding |
--casca-prose-details-padding-inline | var(--casca-size-4) | Disclosure inline padding |
--casca-prose-details-radius | var(--casca-radius-2) | Disclosure corner radius |
--casca-prose-details-marker-color / --casca-prose-details-marker-color-dark | var(--casca-color-1) | Disclosure marker color |
--casca-prose-details-marker-size | 0.75em | Disclosure marker size |
--casca-prose-dl-term-color | var(--casca-prose-heading-color) | Definition term color |
--casca-prose-dl-term-font | var(--casca-prose-heading-font) | Definition term font |
--casca-prose-dl-term-weight | var(--casca-font-weight-7) | Definition term weight |
--casca-prose-dl-term-spacing-above | var(--casca-size-4) | Space before the next term |
--casca-prose-dl-definition-color | var(--casca-prose-color) | Definition text color |
--casca-prose-dl-definition-indent | var(--casca-size-4) | Definition inline indent |
--casca-prose-dl-definition-gap | var(--casca-size-2) | Gap between sibling definitions |
--casca-prose-anchor-color | var(--casca-prose-link-color) | Heading anchor color |
--casca-prose-anchor-color-hover | var(--casca-prose-link-color-hover) | Revealed heading anchor color |
--casca-prose-anchor-opacity | 0 | Resting heading anchor opacity |
--casca-prose-anchor-opacity-revealed | 1 | Hover, focus, and touch heading anchor opacity |
--casca-prose-anchor-gap | var(--casca-size-2) | Gap between heading text and anchor |
--casca-prose-anchor-content | "#" | CSS-emitted visible heading-anchor glyph |
Section Dividers
casca-divider-pixel
Pixel-block decorative rule for editorial section breaks. Renders the
pattern ▓▒░ <label> ░▒▓ with the flanking blocks in an accent color
and the label in a muted color. Replaces <hr> where a softer divider
matches the editorial voice.
<div class="casca-divider-pixel">Charts</div>
<div class="casca-divider-pixel">Controls</div>
<div class="casca-divider-pixel">Components</div>
<!-- Empty label is also valid: a label-less rule -->
<div class="casca-divider-pixel"></div>
The flanking blocks are rendered via ::before / ::after pseudo-elements
with Unicode shade glyphs (U+2593 U+2592 U+2591), so the markup stays
clean and screen readers see only the label content.
casca-divider-pixel CSS Variables
| Variable | Default | Purpose |
|---|---|---|
--casca-divider-pixel-color | var(--casca-color-1) | Pixel-block fill |
--casca-divider-pixel-label-color | var(--casca-gray-6) | Label color (light) |
--casca-divider-pixel-label-color-dark | var(--casca-gray-5) | Label color (dark) |
--casca-divider-pixel-font | var(--casca-font-mono) | Font for blocks + label |
--casca-divider-pixel-block-size | var(--casca-font-size-3) | Pixel-block size |
--casca-divider-pixel-label-size | var(--casca-font-size-0) | Label size |
--casca-divider-pixel-gap | var(--casca-size-3) | Gap between blocks and label |
--casca-divider-pixel-margin-block | var(--casca-size-6) | Vertical room around divider |
--casca-divider-pixel-letter-spacing | 0.18em | Letter-spacing for the label |
Contract notes
- The blocks render reliably in any monospace font; Departure Mono (Casca's display font, wired in 3.5a) renders them pixel-perfect.
- Forced-colors mode replaces the accent color with
CanvasTextso the blocks remain visible in Windows High Contrast.
Layout Primitives
casca-layout
<casca-layout class="casca"> is the Declarative Shadow DOM page-layout
primitive. It is an unregistered custom-element name, so Casca ships no
customElements.define() call and no JavaScript polyfill. The primitive ships
in dist/casca.css, alongside its header,
nav, picker, hero, and footer chrome dependencies.
For whole-page Casca documents, use <body class="casca"> and make
<casca-layout> the first substantive body child. body.casca is the core
bundle's page-shell opt-in: it resets the browser body margin, paints the page
background and ink from Casca semantic tokens, and sets min-block-size: 100vh.
Subtree integrations that use .casca on a wrapper are not affected.
The .casca-layout-skip skip-link class works in any context, not just
inside the <casca-layout> shadow DOM. Drop it as the first element in
<body> of a hand-authored layout and pair it with id="main" on the
target element for a WCAG 2.4.1 "Bypass Blocks" skip link. The class
provides visually-hidden positioning that reveals on focus.
The host element must carry class="casca". That class is part of the public
contract, not decoration: it anchors the mood overlays and the no-JS theme
picker preview selector.
Load the same dist/casca.css URL in the outer document and inside the
template. The full-page starter lives at
tools/release-templates/casca-layout-shell.html.
<body class="casca">
<casca-layout class="casca" data-variant="doc">
<template shadowrootmode="open">
<link rel="stylesheet" href="/dist/casca.css">
<a class="casca-layout-skip" href="#casca-layout-main">
Skip to main content
</a>
<header class="casca-site-header">
<slot name="brand">
<a class="casca-site-header-brand" href="/">Site</a>
</slot>
<nav class="casca-anchor-nav" data-orientation="horizontal"
aria-label="Sections">
<slot name="nav"></slot>
</nav>
<slot name="theme-picker"></slot>
</header>
<slot name="hero"></slot>
<main>
<slot></slot>
</main>
<footer class="casca-site-footer">
<slot name="footer"></slot>
</footer>
</template>
<a slot="brand" class="casca-site-header-brand" href="/">Site</a>
<a slot="nav"
class="casca-anchor-nav-link casca-anchor-nav-link--in-header"
href="/">Home</a>
<a slot="nav"
class="casca-anchor-nav-link casca-anchor-nav-link--in-header"
href="/docs/">Docs</a>
<form slot="theme-picker"
class="casca-theme-picker casca-theme-picker--in-header"
data-casca-theme-picker
method="get" action="/_casca/theme">
<!-- picker radios -->
</form>
<section slot="hero" class="casca-hero">
<h1>Landing hero</h1>
</section>
<article id="casca-layout-main" tabindex="-1" class="casca-prose">
<h1>Page heading</h1>
<p>Main content fills the unnamed default slot.</p>
</article>
<footer slot="footer">
<p class="casca-site-footer-row">
<span>Updated locally</span>
</p>
</footer>
</casca-layout>
</body>
Slot contract:
| Slot | Purpose |
|---|---|
brand | Header brand slot. The template provides fallback text. |
nav | Header navigation links. |
theme-picker | Header mood picker form. Keep it in light DOM so :has() preview works. |
hero | Landing-only hero area. Non-landing variants suppress the shadow slot. |
footer | Footer content. The template provides fallback text. |
| unnamed default | Main content rendered inside the shadow <main>. |
Variant contract:
data-variant | Main width | Typography | Notes |
|---|---|---|---|
omitted or doc | 50rem | Prose-like | Default. |
article | 38rem | Prose-like | Narrow editorial column. |
demo | 64rem | Casca sans | Wider component demo surface. |
landing | 80rem | Casca sans | Renders the hero slot above main. |
Slotted header children must carry explicit modifier classes because the
.casca-site-header ... descendant chains do not cross the shadow boundary:
use .casca-anchor-nav-link--in-header on each slotted nav anchor and
.casca-theme-picker--in-header on the slotted picker form.
The skip link points to #casca-layout-main. Put that id and
tabindex="-1" on the consumer's light-DOM main-slot wrapper, commonly
<article class="casca-prose">. The id is not authored inside the shadow
template; light-DOM fragment targets work consistently across engines, and the
tabindex value allows focus transfer without adding the wrapper to normal Tab
order.
casca-layout CSS Variables
| Variable | Default | Purpose |
|---|---|---|
--casca-layout-doc-max-inline | 50rem | Default and doc main width |
--casca-layout-article-max-inline | 38rem | article main width |
--casca-layout-demo-max-inline | 64rem | demo main width |
--casca-layout-landing-max-inline | 80rem | landing main width |
--casca-layout-padding-block-start | var(--casca-size-5) | Main block-start padding |
--casca-layout-padding-block-end | var(--casca-size-6) | Main block-end padding |
--casca-layout-padding-inline | var(--casca-size-3) | Main inline padding |
--casca-layout-gap | var(--casca-size-4) | Landing hero to main gap |
--casca-layout-header-bg | var(--casca-surface-1) | Header background |
--casca-layout-header-border | var(--casca-rule) | Header block-end rule |
--casca-layout-header-block-padding | var(--casca-size-2) | Header vertical padding |
--casca-layout-footer-bg | var(--casca-surface-1) | Footer background |
--casca-layout-footer-border | var(--casca-rule) | Footer block-start rule |
--casca-layout-footer-block-padding | var(--casca-size-5) | Footer vertical padding |
casca-theme-image
A layout primitive for painting any raster image as a single-color
silhouette that follows the active theme. The image becomes a CSS
mask; the fill is currentColor by default, so the silhouette picks
up whatever ink the parent surface is carrying for the active mood.
Use for nav home buttons, logo links inside theme-scoped chrome, or
any pixel/raster brand mark that needs to read as a single mood-
appropriate color rather than its original palette.
The intended host is a <span> (or <i>, <svg>) inside an
interactive ancestor. The accessible name lives on the ancestor via
aria-label; the silhouette span is aria-hidden. This avoids the
dual-source problem of putting the mask on <img> (where the raster
paint and the mask paint compete).
<a class="casca-nav-home" href="/" aria-label="Casca home">
<span class="casca-theme-image"
style="--casca-theme-image-src: url('/img/casca-px-header-128.png');
inline-size: 4.5rem; block-size: 1.5rem"
aria-hidden="true"></span>
</a>
casca-theme-image CSS Variables
| Variable | Default | Purpose |
|---|---|---|
--casca-theme-image-src | (none) | Mask image (set per-instance) |
--casca-theme-image-color | currentColor | Silhouette fill color |
Contract notes
- The host element gets
image-rendering: pixelatedso bitmap brand marks stay crisp under integer scaling. Vector mask sources stay smooth regardless. - Forced-colors mode drops the mask and falls back to a transparent
background with
color: CanvasText, so the silhouette renders in the system's high-contrast ink. - The accessible name MUST live on an interactive ancestor; the silhouette element itself is decorative.
casca-site-header
Sticky top-bar shell with three slots: brand (left), anchor-nav (middle), theme picker (right). Designed for the portal pattern in DESIGN.md §Sticky top bar.
<header class="casca-site-header">
<a class="casca-site-header-brand" href="/">
<img src="..." width="71" height="24" alt="Brand">
</a>
<nav class="casca-anchor-nav" data-orientation="horizontal">
<a class="casca-anchor-nav-link" href="#charts">Charts</a>
<a class="casca-anchor-nav-link" href="#controls">Controls</a>
<a class="casca-anchor-nav-link" href="#components">Components</a>
</nav>
<form class="casca-theme-picker" data-casca-theme-picker>
<!-- ... see casca-theme-picker below ... -->
</form>
</header>
Below 640px the bar wraps to two rows and the anchor-nav scrolls horizontally - no hamburger, no brand-hiding, no JS (DR5). Every interactive element meets 44×44 touch targets (DR6).
casca-site-header CSS Variables
| Variable | Default | Purpose |
|---|---|---|
--casca-site-header-bg | var(--casca-surface-1) | Background |
--casca-site-header-color | var(--casca-gray-9) | Text color (light) |
--casca-site-header-color-dark | var(--casca-gray-0) | Text color (dark) |
--casca-site-header-border | var(--casca-gray-3) | Block-end rule (light) |
--casca-site-header-border-dark | var(--casca-gray-7) | Block-end rule (dark) |
--casca-site-header-pad-block | var(--casca-size-2) | Vertical padding |
--casca-site-header-pad-inline | var(--casca-size-4) | Horizontal padding |
--casca-site-header-gap | var(--casca-size-4) | Gap between slots |
--casca-site-header-min-block | 3.5rem | Minimum block-size |
--casca-site-header-font | var(--casca-font-sans) | Font binding |
casca-theme-picker
CSS-rendered pixel-block mood picker. It is a native GET form containing a
hidden return_to input, a fieldset, and radio labels. No JS, no submit
button: selecting a swatch applies the mood immediately via pure CSS
(:has() + the picker-scoped overlay rules). The form wrapping is
forward-compatibility scaffolding for the future casca serve persistence
endpoint; when that ships, the SSG widget re-emits a submit button so the
form can POST the chosen mood as a cookie.
<form class="casca-theme-picker" data-casca-theme-picker method="get" action="/_casca/theme">
<input type="hidden" name="return_to" value="/docs/">
<fieldset class="casca-theme-picker-fieldset" aria-label="Theme">
<legend class="sr-only">Theme</legend>
<label class="casca-theme-picker-button" data-mood="solar" style="--_swatch-a:#f2ebda;--_swatch-b:#c8633a">
<input type="radio" name="casca-theme" value="solar" checked>
<span class="sr-only">Solar</span>
</label>
<label class="casca-theme-picker-button" data-mood="cyber" style="--_swatch-a:#e8f0e9;--_swatch-b:#1e7a4f">
<input type="radio" name="casca-theme" value="cyber">
<span class="sr-only">Cyber</span>
</label>
</fieldset>
</form>
State vocabulary:
- Hover translates the button up one pixel (no scale; stays on the pixel grid).
- Focus-visible halos the button with the mood's accent color.
- Checked strengthens the border and (on dark themes) adds a phosphor
glow via
box-shadowin the mood's accent.
Mood overlay scoping
The picker primitive and the full picker-scoped mood overlay set ship in
dist/casca.css. Solar is the unscoped base mood; Cyber, Lunar, and
Arcade are scoped through server state and the checked radio in
.casca-theme-picker[data-casca-theme-picker].
The preview selector is rooted at
.casca-theme-picker[data-casca-theme-picker], so unrelated forms cannot
hijack the mood:
body[data-casca-theme="cyber"] .casca {
/* persisted Cyber overrides */
}
@supports selector(:has(*)) {
body:has(.casca-theme-picker[data-casca-theme-picker] input[name="casca-theme"][value="cyber"]:checked) .casca {
/* Cyber overrides */
}
}
casca-theme-picker CSS Variables
| Variable | Default | Purpose |
|---|---|---|
--casca-theme-picker-size | 1.5rem (24px) | Button edge length (desktop) |
--casca-theme-picker-size-touch | 2.75rem (44px) | Button edge length on touch (DR6) |
--casca-theme-picker-gap | var(--casca-size-1) | Gap between buttons |
--casca-theme-picker-radius | var(--casca-radius-1) | Button border-radius |
--casca-theme-picker-border | var(--casca-gray-4) | Unchecked border (light) |
--casca-theme-picker-border-dark | var(--casca-gray-6) | Unchecked border (dark) |
--casca-theme-picker-border-checked | var(--casca-gray-9) | Checked border (light) |
--casca-theme-picker-border-checked-dark | var(--casca-gray-0) | Checked border (dark) |
Per-mood swatch colors live behind [data-mood="..."] attribute selectors
as fallback styling. Manifest-generated picker buttons set --_swatch-a /
--_swatch-b directly on the button.
Contract notes
- Each button label contains a visually-hidden
<span class="sr-only">carrying the mood name. Screen readers announce the choice; visible users see the swatch. - Forced-colors mode replaces the swatches with
ButtonFace/Highlightso the picker remains usable in Windows High Contrast. - Reduced motion drops the hover translate and the checked glow.
- The form action
/_casca/themeis served bycasca devandcasca previewwhen the SSG config includes atheme-pickermanifest. Static hosts need an edge or proxy implementation for the same request-time contract. ?casca-theme=<id>overrides the cookie for one HTML response. The setter writes or clears only the host-onlycasca-themecookie and redirects to a validated same-originreturn_topath.
casca-hero
Slot-based scaffolding for a top-of-page chart-as-hero block. Owns layout
only: figure slot, caption row, divider slot, brand block. Each slot is
consumer-supplied so the same primitive serves a bar chart, a screenshot,
a generated mosaic, or any other visual. Per DR1 the visual is iterated
via /design-shotgun; this primitive ships scaffolding only.
<section class="casca-hero" aria-labelledby="hero-title">
<div class="casca-hero-figure">
<figure class="casca casca-figure casca-bar">
<!-- hero chart ... -->
</figure>
</div>
<p class="casca-hero-caption">
<span>JAN</span><span>FEB</span><span>MAR</span><span>APR</span>
</p>
<div class="casca-divider-pixel">CASCA</div>
<header class="casca-hero-brand">
<img src="/img/casca-px-header-128.png" width="378" height="128" alt="Casca" id="hero-title">
<p class="casca-hero-manifesto">
No-JS data components for server-rendered pages.
</p>
</header>
</section>
All slots are optional. The hero declares container-name: casca-hero
so descendants can container-query against it (e.g. shrink chart axes
when the hero is narrow).
casca-hero CSS Variables
| Variable | Default | Purpose |
|---|---|---|
--casca-hero-max-inline | 64rem | Maximum column width |
--casca-hero-pad-block | var(--casca-size-6) | Top/bottom padding |
--casca-hero-pad-inline | var(--casca-size-4) | Left/right padding |
--casca-hero-gap | var(--casca-size-4) | Gap between slots |
--casca-hero-figure-block | 13.75rem (220px) | Figure slot height (DESIGN.md) |
--casca-hero-caption-font | var(--casca-font-display) | Caption row font (Departure) |
--casca-hero-caption-size | 0.75rem (12px) | Caption font-size |
--casca-hero-caption-color | var(--casca-gray-7) | Caption color (light) |
--casca-hero-caption-color-dark | var(--casca-gray-3) | Caption color (dark) |
--casca-hero-caption-letter | 0.08em | Caption letter-spacing |
--casca-hero-manifesto-font | var(--casca-font-serif) | Manifesto line font (Newsreader) |
--casca-hero-manifesto-size | 1.0625rem | Manifesto font-size |
--casca-hero-manifesto-color | var(--casca-gray-7) | Manifesto color (light) |
--casca-hero-manifesto-color-dark | var(--casca-gray-3) | Manifesto color (dark) |
--casca-hero-brand-gap | var(--casca-size-2) | Gap between brand image + manifesto |
Contract notes
- Bare
.casca-herois a centered column. All five slot classes are optional and may be reordered freely. - The figure slot stretches any direct child to fill its block-size, so
a chart with its own
block-sizestill respects the hero geometry. - Below 40rem container width the figure slot tightens to 176px and the caption row scrolls horizontally instead of wrapping.
casca-site-footer
Manifesto footer pattern from DESIGN.md §Footer: a Departure-Mono row of
pill-separated phrases plus a single Newsreader-italic ethics line. The
separator · (U+00B7) is generated between consecutive <span> children
via ::before, so the markup carries only the phrases (no separator
nodes).
<footer class="casca-site-footer">
<p class="casca-site-footer-row">
<span>Self-hosted</span>
<span>Decentralized</span>
<span>No telemetry</span>
<span>MIT</span>
</p>
<p class="casca-site-footer-line">
Built principled. Casca is an AC3 project: by and for AIs that are
anti-big-tech, anti-centralized.
</p>
</footer>
Per DESIGN.md the ethics-line copy is TBD with the user; the primitive ships only the layout + typography shell.
casca-site-footer CSS Variables
| Variable | Default | Purpose |
|---|---|---|
--casca-site-footer-bg | var(--casca-surface-1) | Background |
--casca-site-footer-color | var(--casca-gray-7) | Default text color (light) |
--casca-site-footer-color-dark | var(--casca-gray-3) | Default text color (dark) |
--casca-site-footer-border | var(--casca-gray-3) | Block-start rule (light) |
--casca-site-footer-border-dark | var(--casca-gray-7) | Block-start rule (dark) |
--casca-site-footer-pad-block | var(--casca-size-5) | Top/bottom padding |
--casca-site-footer-pad-inline | var(--casca-size-4) | Left/right padding |
--casca-site-footer-gap | var(--casca-size-2) | Gap between row + line |
--casca-site-footer-row-font | var(--casca-font-display) | Row font (Departure Mono) |
--casca-site-footer-row-size | 0.75rem (12px) | Row font-size |
--casca-site-footer-row-color | var(--casca-gray-9) | Row color (light) |
--casca-site-footer-row-color-dark | var(--casca-gray-0) | Row color (dark) |
--casca-site-footer-row-letter | 0.12em | Row letter-spacing |
--casca-site-footer-row-gap | var(--casca-size-2) | Gap between phrases |
--casca-site-footer-row-separator | "\B7" (U+00B7) | Pip character between phrases |
--casca-site-footer-line-font | var(--casca-font-serif) | Italic line font (Newsreader) |
--casca-site-footer-line-size | 1rem | Italic line font-size |
--casca-site-footer-line-color | var(--casca-gray-7) | Italic line color (light) |
--casca-site-footer-line-color-dark | var(--casca-gray-3) | Italic line color (dark) |
--casca-site-footer-line-max | 36rem | Italic line max-inline-size |
Contract notes
- Override the separator pip by setting
--casca-site-footer-row-separatorto any CSS string (e.g."\2014"for an em-dash,"|"for a pipe). - Forced-colors mode drops the surface paint and falls back to
Canvas/CanvasText, keeping the footer legible in Windows High Contrast. - The italic line stays max-inline-size constrained at 36rem so it doesn't sprawl on wide viewports.
UI Components
Chip/Label Overlays
NEW in v0.2.0
<!-- Default chip -->
<div class="casca-chip">Default</div>
<!-- Variant chips -->
<div class="casca-chip" data-variant="success">✓ Success</div>
<div class="casca-chip" data-variant="warning">⚠ Warning</div>
<div class="casca-chip" data-variant="error">✗ Error</div>
<div class="casca-chip" data-variant="info">ℹ Info</div>
<div class="casca-chip" data-variant="outline">Outline</div>
<div class="casca-chip" data-variant="neutral">core</div>
<!-- Absolute positioned chip -->
<div class="casca-chip"
data-position="absolute"
data-variant="success"
style="--chip-top: 1rem; --chip-right: 1rem;">
Healthy
</div>
Chip Attributes:
| Attribute | Values | Description |
|---|---|---|
data-variant | success, warning, error, info, outline, neutral | Chip color scheme (neutral = filled, low-emphasis category/count tag; meaning rides on the text, not color) |
data-position | absolute | Enable absolute positioning |
Chip CSS Variables:
| Variable | Default | Description |
|---|---|---|
--chip-bg | var(--casca-gray-9) | Background color |
--chip-color | var(--casca-gray-0) | Text color |
--chip-border | transparent | Border color |
--chip-top | auto | Top position (absolute) |
--chip-right | auto | Right position (absolute) |
--chip-bottom | auto | Bottom position (absolute) |
--chip-left | auto | Left position (absolute) |
Paginated Legend (CSS-only)
NEW in v0.2.0
<div class="casca-legend-paginated">
<!-- Radio buttons (hidden) -->
<input type="radio" name="legend-pagination" id="legend-page-1" data-page="1" checked>
<input type="radio" name="legend-pagination" id="legend-page-2" data-page="2">
<input type="radio" name="legend-pagination" id="legend-page-3" data-page="3">
<!-- Legend pages -->
<div class="casca-legend-pages">
<div class="casca-legend-page" data-page="1">
<ul class="casca-legend">
<li class="casca-legend-item">
<span class="casca-legend-swatch" aria-hidden="true" style="--_color: var(--casca-color-1)"></span>
Item 1
</li>
<!-- More items -->
</ul>
</div>
<div class="casca-legend-page" data-page="2">
<!-- Page 2 items -->
</div>
<div class="casca-legend-page" data-page="3">
<!-- Page 3 items -->
</div>
</div>
<!-- Pagination controls -->
<div class="casca-legend-controls">
<div class="casca-legend-nav">
<label for="legend-page-1">1</label>
<label for="legend-page-2">2</label>
<label for="legend-page-3">3</label>
</div>
<!-- Dynamic page indicator (updates with :has()) -->
<div>
<span class="casca-legend-indicator-page" data-page="1">Page 1 of 3</span>
<span class="casca-legend-indicator-page" data-page="2">Page 2 of 3</span>
<span class="casca-legend-indicator-page" data-page="3">Page 3 of 3</span>
<!-- Fallback for browsers without :has() support -->
<span class="casca-legend-indicator">Pages: 1-3</span>
</div>
</div>
</div>
Requirements:
- Radio buttons must have unique
idattributes - Radio buttons must share the same
nameattribute - Radio buttons must have
data-page="N"attributes matching their page number (enables flexible ID patterns) .casca-legend-pageelements must have correspondingdata-pageattributes.casca-legend-indicator-pageelements must have correspondingdata-pageattributes (for dynamic indicator)- First radio should have
checkedattribute - Page indicator uses CSS
:has()for dynamic updates (with fallback for older browsers)
ID Pattern Flexibility:
- The component supports any radio
idpattern (e.g.,legend-page-1orlegend-page-myChart-1) - Page visibility and indicator updates are driven by the
data-pageattribute, not the ID - This allows multiple paginated legends on the same page with different ID prefixes
Simple Legend
<ul class="casca-legend">
<li class="casca-legend-item">
<span class="casca-legend-swatch" aria-hidden="true" style="--_color: var(--casca-color-1)"></span>
Label
</li>
</ul>
Charts in Interactive Controls
NEW in v0.2.0
<!-- Chart as button -->
<button class="casca casca-figure casca-interactive">
<h3 class="casca-title">Click to View</h3>
<div class="casca-bar" aria-hidden="true">
<!-- Chart content -->
</div>
</button>
<!-- Chart as link -->
<a href="/details" class="casca casca-figure casca-interactive">
<h3 class="casca-title">View Report →</h3>
<div class="casca-gauge" role="img" aria-label="Report progress 78 percent"
style="--value: 78">
<div class="casca-gauge-value">78%</div>
</div>
</a>
Classes:
.casca-interactive- Adds hover/focus elevation effects
Split-Pill Pager
NEW in v0.2.0
CSS-only pagination component with split-pill design. Renders as two joined halves when both prev/next exist, or as a single full pill when only one button is present.
<!-- Both prev and next (split pill with floating label) -->
<div class="casca-pager">
<a href="#page-1" class="casca-pager-btn casca-pager-btn-prev">← Prev</a>
<span class="casca-pager-label">Page 2 of 5</span>
<a href="#page-3" class="casca-pager-btn casca-pager-btn-next">Next →</a>
</div>
<!-- Prev only (full pill via :only-of-type) -->
<div class="casca-pager">
<a href="#page-4" class="casca-pager-btn casca-pager-btn-prev">← Previous Page</a>
</div>
<!-- Next only (full pill via :only-of-type) -->
<div class="casca-pager">
<a href="#page-2" class="casca-pager-btn casca-pager-btn-next">Next Page →</a>
</div>
<!-- As form buttons (htmx-compatible, NO-JS) -->
<form action="/api/pagination" method="get">
<div class="casca-pager">
<button type="submit" name="page" value="1" class="casca-pager-btn casca-pager-btn-prev">← Prev</button>
<span class="casca-pager-label">3 / 8</span>
<button type="submit" name="page" value="3" class="casca-pager-btn casca-pager-btn-next">Next →</button>
</div>
</form>
<!-- Disabled states -->
<div class="casca-pager">
<button type="button" disabled class="casca-pager-btn casca-pager-btn-prev">← Prev</button>
<span class="casca-pager-label">Page 1 of 10</span>
<a href="#page-2" class="casca-pager-btn casca-pager-btn-next">Next →</a>
</div>
Classes:
| Class | Description |
|---|---|
.casca-pager | Container for pager buttons and label |
.casca-pager-btn | Base button/link class (required) |
.casca-pager-btn-prev | Left half (rounded left corners) |
.casca-pager-btn-next | Right half (rounded right corners) |
.casca-pager-label | Floating center label (absolute positioned, z-index: 10) |
Features:
- ✅ Works with both
<a>links and<button type="submit">(NO-JS friendly) - ✅ Automatic full pill when only one button via
:only-of-type - ✅ Independent hover states for each half
- ✅ Keyboard accessible (focus-visible states)
- ✅ Disabled state support
- ✅ Floating center label doesn't affect layout (pointer-events: none)
States:
:hover- Scale transform + background change:active- Pressed state:focus-visible- Keyboard focus outline:disabled/[aria-disabled="true"]- Dimmed, non-interactive
Axis Cycler Usage:
The .casca-pager component can also be used as an axis cycler by changing the labels:
<!-- Time axis cycler -->
<div class="casca-pager">
<button type="submit" name="axis" value="day" class="casca-pager-btn casca-pager-btn-prev">← Day</button>
<span class="casca-pager-label">Week</span>
<button type="submit" name="axis" value="month" class="casca-pager-btn casca-pager-btn-next">Month →</button>
</div>
<!-- Chart type cycler -->
<div class="casca-pager">
<a href="?view=bar" class="casca-pager-btn casca-pager-btn-prev">← Bar</a>
<span class="casca-pager-label">Line</span>
<a href="?view=area" class="casca-pager-btn casca-pager-btn-next">Area →</a>
</div>
The same split-pill design and interaction patterns apply.
Legend Row
NEW in v0.2.0
Grid-based legend row component with label truncation and right-aligned chip rail. Ideal for CASH FLOW navigators and interactive legend lists.
<!-- Static (non-interactive) -->
<div class="casca-legend-row">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-1)"></span>
<span class="casca-legend-row-label">Category A: Products</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$45k</span>
<span class="casca-chip">32%</span>
</div>
</div>
<!-- Long label with truncation -->
<div class="casca-legend-row">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-2)"></span>
<span class="casca-legend-row-label">Very Long Category Name That Truncates With Ellipsis...</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$28k</span>
<span class="casca-chip">18%</span>
</div>
</div>
<!-- Interactive as button -->
<button type="button" class="casca-legend-row">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-3)"></span>
<span class="casca-legend-row-label">Category C: Services</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$52k</span>
<span class="casca-chip">38%</span>
</div>
</button>
<!-- Interactive as link -->
<a href="#category-d" class="casca-legend-row">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-4)"></span>
<span class="casca-legend-row-label">Category D: Licensing Revenue</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$12k</span>
<span class="casca-chip">8%</span>
</div>
</a>
<!-- Selected state -->
<button type="button" class="casca-legend-row" data-selected="true">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-5)"></span>
<span class="casca-legend-row-label">Category E: Consulting (Active)</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$67k</span>
<span class="casca-chip">48%</span>
</div>
</button>
<!-- Current (aria-current) -->
<a href="#category-h" class="casca-legend-row" aria-current="true">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-8)"></span>
<span class="casca-legend-row-label">Category H: Current Quarter Focus</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$89k</span>
<span class="casca-chip" data-variant="success">↑ 12%</span>
</div>
</a>
<!-- Disabled -->
<button type="button" class="casca-legend-row" disabled>
<span class="casca-legend-swatch" style="--_color: var(--casca-color-7)"></span>
<span class="casca-legend-row-label">Category G: Archived Projects</span>
<div class="casca-legend-row-rail">
<span class="casca-chip">$0</span>
<span class="casca-chip">0%</span>
</div>
</button>
Classes:
| Class | Description |
|---|---|
.casca-legend-row | Container (CSS Grid: auto 1fr auto) |
.casca-legend-row-label | Middle column, truncates with ellipsis (min-width: 0) |
.casca-legend-row-rail | Right column, flex container for chips (flex-shrink: 0) |
.casca-legend-swatch | Color indicator (left column) |
Attributes:
| Attribute | Values | Description |
|---|---|---|
href | URL | Makes row interactive (link) |
type | button, submit | Makes row interactive (button) |
data-selected | "true" | Selected/emphasized state |
aria-current | "true", "page" | Current item (emphasized) |
disabled | - | Disabled state |
aria-disabled | "true" | Disabled state (non-native elements) |
Features:
- ✅ Three-column grid layout (swatch + label + chip rail)
- ✅ Label truncates with ellipsis without breaking chip rail
- ✅ Works as
<div>,<button>, or<a>(semantic flexibility) - ✅ Chip rail never shrinks (stable right alignment)
- ✅ Keyboard accessible (focus-visible)
- ✅ Screen reader friendly
States:
:hover- Background highlight (interactive only):active- Pressed background:focus-visible- Keyboard focus outline[data-selected="true"]/[aria-current]- Emphasized with brand color:disabled/[aria-disabled]- Dimmed, non-interactive
Layout Details:
- Grid columns:
auto 1fr auto(swatch | label | rail) - Label:
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0 - Rail:
flex-shrink: 0ensures chips never compress
Dashboard use case:
Combine legend rows with pager for multi-month navigation:
<div style="border: 1px solid var(--casca-gray-3); border-radius: var(--casca-radius-2); overflow: hidden;">
<a href="#jan-2025" class="casca-legend-row" aria-current="true">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-5)"></span>
<span class="casca-legend-row-label">January 2025</span>
<div class="casca-legend-row-rail">
<span class="casca-chip" data-variant="success">+$15k</span>
<span class="casca-chip">68%</span>
</div>
</a>
<a href="#feb-2025" class="casca-legend-row">
<span class="casca-legend-swatch" style="--_color: var(--casca-color-8)"></span>
<span class="casca-legend-row-label">February 2025</span>
<div class="casca-legend-row-rail">
<span class="casca-chip" data-variant="error">-$8k</span>
<span class="casca-chip">36%</span>
</div>
</a>
</div>
<div class="casca-pager">
<button type="submit" name="quarter" value="Q4-2024" class="casca-pager-btn casca-pager-btn-prev">← Q4 2024</button>
<span class="casca-pager-label">Q1 2025</span>
<button type="submit" name="quarter" value="Q2-2025" class="casca-pager-btn casca-pager-btn-next">Q2 2025 →</button>
</div>
Slot Grid (form-aware, no-JS)
NEW in v0.3.0 · data-state="pending" added in v0.9.0
A CSS-only, FORM-AWARE grid of selectable time slots, laid out as day/resource
columns x time rows. Each slot is a real <input type="radio"> wrapped in a
styled <label>, so selection, keyboard navigation, and form submission work
with zero JavaScript.
Unlike the read-only data charts, the visual grid is the form control: there
is no separate aria-hidden visual + hidden data table. The slots are native
inputs and are accessible by default. Do not aria-hidden it.
<form method="post" action="/book">
<fieldset class="casca-slot-grid" style="--casca-slot-cols: 5">
<legend class="casca-slot-grid-title">Choose a time</legend>
<div class="casca-slot-col">
<span class="casca-slot-colhead">Mon 27</span>
<label class="casca-slot">
<input class="casca-slot-input" type="radio" name="slot"
value="2026-05-27T09:00:00Z/2026-05-27T09:30:00Z">
<span class="casca-slot-time">09:00</span>
</label>
<label class="casca-slot" data-state="taken">
<input class="casca-slot-input" type="radio" name="slot"
value="2026-05-27T09:30:00Z/2026-05-27T10:00:00Z" disabled>
<span class="casca-slot-time">09:30</span>
</label>
<label class="casca-slot" data-state="pending">
<input class="casca-slot-input" type="radio" name="slot"
value="2026-05-27T10:00:00Z/2026-05-27T10:30:00Z" disabled>
<span class="casca-slot-time">10:00</span>
</label>
<!-- ... more slots ... -->
</div>
<!-- ... more day columns ... -->
</fieldset>
<button type="submit">Book this time</button>
</form>
Classes:
| Class | Description |
|---|---|
.casca-slot-grid | The <fieldset> container (CSS Grid; max columns = --casca-slot-cols) |
.casca-slot-grid-title | The <legend>; spans the full grid width |
.casca-slot-col | A single day / resource column (vertical stack of slots) |
.casca-slot-colhead | Column header text (e.g. "Mon 27") |
.casca-slot | A selectable slot; the <label> wrapping the radio |
.casca-slot-input | The native <input type="radio"> (visually hidden, still focusable) |
.casca-slot-time | The visible time label inside a slot |
Attributes:
| Attribute | On | Values | Description |
|---|---|---|---|
--casca-slot-cols | .casca-slot-grid (inline custom property) | integer | Maximum number of columns; collapses below this as the grid narrows (default 5) |
data-state | .casca-slot | "taken" | "pending" | "taken" = dimmed, dashed, struck-through; "pending" = on-hold / awaiting confirmation (amber, solid border). Both non-interactive |
disabled | .casca-slot-input | - | Native disabled state (also dims the slot via :has()); set it on taken and pending slots so they are never submitted |
name / value | .casca-slot-input | string | Standard radio name + submitted value |
States (all pure CSS):
- default - available / selectable
:checked(via.casca-slot:has(.casca-slot-input:checked)) - selected, brand-color highlight:hover- subtle background on selectable slots only:focus-visible(via:has()) - keyboard focus ring on the slot[data-state="taken"]/:disabled- dimmed, dashed border, strikethrough, not selectable[data-state="pending"]- on hold / awaiting confirmation: amber fill, solid border, no strikethrough, not selectable
Features:
- ✅ Native radio group inside
<fieldset>/<legend>- keyboard accessible out of the box (Tab in, arrow keys move, Space/Enter selects) - ✅ Single-select with standard form submission - no JS
- ✅ ≥44px touch targets (
--casca-slot-min-size) - ✅ Columns stack to a single column on narrow viewports (container query, viewport-query fallback)
- ✅ Dark-mode aware; works from the core build with consumer-supplied token mappings
- ✅ Reduced-motion, high-contrast, and forced-colors handling
Slot Grid CSS Variables:
| Variable | Default | Description |
|---|---|---|
--casca-slot-cols | 5 | Maximum column count; columns collapse below this as the grid narrows |
--casca-slot-col-min | 4.5rem | Minimum column width before columns collapse (responsive threshold) |
--casca-slot-gap | --casca-size-2 | Gap between columns and slots |
--casca-slot-radius | --casca-radius-2 | Slot corner radius |
--casca-slot-min-size | 2.75rem | Minimum slot size (44px touch target) |
--casca-slot-bg / -color / -border | grays | Available slot colors (light) |
--casca-slot-hover-bg / -border | grays | Hover colors (light) |
--casca-slot-selected-bg / -border | --casca-color-1 | Selected fill / border (accent); auto-lightens in dark |
--casca-slot-selected-color | --casca-on-accent | Selected label color; tracks the accent foreground (white in light, dark in dark-mode themes) |
--casca-slot-taken-bg / -color | grays | Taken / disabled colors (light) |
--casca-slot-pending-bg / -color / -border | yellow family | Pending / hold colors (light); defaults to the casca yellow ramp, map to your scheduling palette |
--casca-slot-focus-ring | --casca-color-1 | Focus outline color |
--casca-slot-*-dark | grays / yellow | Dark-mode overrides for bg/color/border/hover/taken/pending |
See site/pages/controls/slot-grid.html and site/assets/integrations/go-templates/slot-grid.tmpl.
Analytics Block
The analytics block renders symmetrics site-summary.json as an in-theme,
server-rendered dashboard or homepage summary. It ships in dist/casca.css,
uses no JavaScript, and keeps state in ordinary HTML attributes.
<section class="casca casca-analytics"
data-casca-analytics-variant="dashboard"
data-casca-analytics-state="preliminary"
data-casca-analytics-registers="public ops"
aria-labelledby="analytics-title"
aria-describedby="analytics-summary">
<header class="casca-analytics-hero">
<p class="casca-analytics-kicker">Traffic, counted without tracking</p>
<h1 id="analytics-title" class="casca-analytics-headline">
<span class="casca-analytics-value">2,467</span>
<span class="casca-analytics-unit">page views</span>
<span class="casca-analytics-date">Sat May 30</span>
</h1>
<p class="casca-analytics-lede">We count traffic without tracking you.</p>
</header>
<p id="analytics-summary" class="casca-data">
Seven-day trend includes a preliminary day. Sat May 30 is still being tallied and is not final.
</p>
<div class="casca-analytics-trend">
<table class="casca-analytics-trend-table">
<caption>Seven-day page-view trend</caption>
<tbody>
<tr data-casca-analytics-day-state="preliminary">
<th scope="row">Sat May 30</th>
<td class="casca-analytics-count">2,467</td>
<td>
<span class="casca-analytics-bar" aria-hidden="true" style="--casca-analytics-bar-size: 100%"></span>
<span class="casca-analytics-day-note">still tallying</span>
</td>
</tr>
</tbody>
</table>
</div>
</section>
The summary variant uses the same root and state attributes with
data-casca-analytics-variant="summary", a compact heading, a one-line status,
and the same .casca-data state summary.
Classes / attributes:
| Class / attribute | On | Description |
|---|---|---|
.casca-analytics | root section | Analytics component root, used with .casca |
.casca-analytics-hero | header | Public lead section |
.casca-analytics-kicker | text | Small register label |
.casca-analytics-headline | heading | State heading |
.casca-analytics-value | span | Numeric value with tabular figures |
.casca-analytics-unit | span | Unit text, usually page views |
.casca-analytics-date | span | Public date label |
.casca-analytics-lede | paragraph | Public ethics line |
.casca-analytics-checklist | list | Non-extractive checklist |
.casca-analytics-state-note | paragraph | Visible state note |
.casca-analytics-trend | div | Trend table wrapper |
.casca-analytics-trend-table | table | Visible semantic trend table |
.casca-analytics-count | table cell | Numeric count cell |
.casca-analytics-bar | span | Presentational bar, aria-hidden="true" |
.casca-analytics-day-note | span | Visible day status |
.casca-analytics-registers | section | Public and ops register wrapper |
.casca-analytics-register | div | One register panel |
.casca-analytics-register-title | heading | Register heading |
.casca-analytics-status | paragraph | Compact summary status |
.casca-analytics-notice | section | Integrity-fail notice body |
.casca-analytics-notice-title | heading | Integrity-fail title |
.casca-analytics-notice-body | paragraph | Integrity-fail explanation |
.casca-analytics-last-good | paragraph | Last verified timestamp line |
.casca-analytics-verify | footer | Provenance footer |
.casca-analytics-verify-link | link | Verification link |
.casca-analytics-verify-recipe | block | Optional plain verification steps |
data-casca-analytics-variant="dashboard | summary" | root | Layout variant |
data-casca-analytics-state="ok | never-published | zero-traffic | single-day | stale | preliminary | integrity-fail" | root | Rendered state |
data-casca-analytics-cause="missing-feed | ttl-exceeded | publisher-timestamp-behind | hash-mismatch | signature-invalid | signature-absent | schema-mismatch | json-parse | field-invalid" | root | Optional state cause |
data-casca-analytics-register="public | ops" | register | Register vocabulary |
data-casca-analytics-day-state="sealed | preliminary" | trend row | Day finality |
--casca-analytics-bar-size: NN% | bar inline style | Server-normalized bar size |
CSS variables: --casca-analytics-bg, --casca-analytics-surface,
--casca-analytics-border, --casca-analytics-radius,
--casca-analytics-pad-block, --casca-analytics-pad-inline,
--casca-analytics-gap, --casca-analytics-max-inline,
--casca-analytics-display-font, --casca-analytics-body-font,
--casca-analytics-ui-font, --casca-analytics-mono-font,
--casca-analytics-value-size, --casca-analytics-summary-value-size,
--casca-analytics-label-size, --casca-analytics-color,
--casca-analytics-muted, --casca-analytics-rule,
--casca-analytics-accent, --casca-analytics-on-accent,
--casca-analytics-link-color, --casca-analytics-link-decoration,
--casca-analytics-state-border, --casca-analytics-state-accent,
--casca-analytics-trend-row-gap, --casca-analytics-bar-block-size,
--casca-analytics-bar-radius, --casca-analytics-bar-track,
--casca-analytics-bar-color, --casca-analytics-bar-size,
--casca-analytics-preliminary-fill, --casca-analytics-preliminary-border,
--casca-analytics-preliminary-hatch, --casca-analytics-integrity-bg,
--casca-analytics-integrity-border, --casca-analytics-integrity-accent,
--casca-analytics-verify-min-block-size, and
--casca-analytics-verify-pad-inline.
Preprocessor config:
[preprocessors.symmetrics]
kind = "symmetrics"
source = ".well-known/symmetrics/example.com/site-summary.json"
expected_schema_version = "symmetrics-report.v1"
cache_key = "symmetrics:example.com"
required = false
[preprocessors.symmetrics.target]
page = "/metrics/"
selector = "[data-casca-slot='analytics']"
action = "replace_content"
The implementation uses only common preprocessor fields. The dashboard variant
is the default. A summary variant is selected by convention when the
preprocessor name, target selector, or page URL contains summary.
Analytics Block live layer (000084 opt-in)
The optional dist/casca-live.js asset polls a JSON sidecar emitted by the
symmetrics preprocessor and applies field-level text swaps in place. The
static render is complete and correct without the script. The asset is built
by make build-live (not by the default make build) and is NOT bundled
into any CSS file.
Host opt-in. Two layers are required:
- Per-site (asset shipping). Set
[build] live_layer = trueincasca.toml. Whentrue, the symmetrics preprocessor emits a per-block JSON sidecar at_casca/preprocessors/symmetrics/<cache_key>.json. Whenfalse(the default), no sidecar is emitted and the analytics block remains purely static. - Per-block (script activation). The host page template adds
data-casca-live-source="/_casca/preprocessors/symmetrics/<cache_key>.json"on the<section class="casca casca-analytics">element. Optionally,data-casca-live-interval="N"overrides the 60-second default poll cadence (clamped to 600 seconds on backoff).
The host also loads the script with a single tag:
<script src="/dist/casca-live.js" defer></script>
Kill switch. Removing the <script> tag, or removing the
data-casca-live-source attribute, fully disables the live behavior on a
block. The static render continues to serve correctly.
Optional aria-live opt-in. The host MAY add aria-live="polite" on a
single <p class="casca-analytics-status"> node within the block. The
script's text swap on that node will fire a polite screen-reader
announcement. The script never adds aria-live itself and never sets
aria-live="assertive" or role="alert". Default is no announcement.
JSON sidecar shape (casca-live.v1):
{
"schema_version": "casca-live.v1",
"state": "ok|never-published|zero-traffic|single-day|stale|preliminary|integrity-fail",
"cause": "hash-mismatch|signature-invalid|...|null",
"variant": "dashboard|summary",
"value": "2,467",
"unit": "page views",
"date": "Sat May 30",
"state_note": "Today is still being tallied.",
"status": "Last updated 3 days ago.",
"last_good": "Sat May 27: 2,401 page views.",
"trend_rows": [
{ "day_state": "sealed", "count": "2,467", "bar_size": "84", "note": "Sat May 30", "active": true }
],
"data_summary": "Sat May 30: 2,467 page views. Stable trend.",
"generated_at": 1748563200
}
Integrity-fail field-clearing rule. When state == "integrity-fail",
the server emits empty strings for value, unit, date, last_good,
and every trend_rows[].count / note / bar_size. The script is a
dumb assigner: it writes whatever the sidecar carries. The server is the
single integrity authority. The script has no per-state override for
these fields; this keeps the integrity contract centralized server-side.
What the script does (and does not do). The script writes
textContent on named field spans, setAttribute on the root
data-casca-analytics-state and data-casca-analytics-cause, and
element.style.setProperty('--casca-analytics-bar-size', ...) on bar
elements. The script source contains NO .innerHTML = assignments, NO
Node.replaceWith() / Node.replaceChildren() calls, NO
Element.outerHTML = assignments, NO focus() calls, and NO
tabindex writes. The grep gate is enforced in CI. Element identity for
the <section>, the <table>, every row, and the verify link is
preserved across every state transition; CSS handles the per-state
visibility flip via the [data-casca-analytics-state-block~="X"] gate
added to src/charts/analytics.css.
v1 budget concessions (design §13.4). The shipped script omits trend-
row text/bar updates and the visibilitychange / online pause-resume
handlers so it fits the 2 KB minified / 1 KB gzipped budget. The static
render of the trend table remains complete; only the live-text swap on
trend cells is deferred to a later version. The headline value/unit/date,
the per-state note/status/last-good, the root state attribute, the cause
attribute, and the .casca-data text alternative all do swap live.
Browser floor. fetch, AbortController, Promise, JSON.parse,
querySelectorAll, textContent, setAttribute, and the hidden
attribute. Chrome 66+, Edge 16+, Firefox 57+, Safari 12.1+. Older
browsers see the static render only and never break.
Data Table (visible chart companion)
NEW in v0.10.0
A styled, accessible native <table> - the visible companion to the charts.
Pure CSS, no JS. Numeric columns align with tabular-nums; supports zebra
striping, an opt-in sticky header, a totals <tfoot>, and trend cells.
Interactivity composes with the Filter primitive (row filtering);
sort and pagination are server-driven.
<div class="casca-table-wrap"> <!-- optional: scroll + sticky header -->
<table class="casca-table">
<caption class="casca-table-caption">Revenue by product</caption>
<thead>
<tr>
<th scope="col">Product</th>
<th scope="col" class="casca-table-num">Revenue</th>
<th scope="col" class="casca-table-num">YoY</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Atlas</th>
<td class="casca-table-num">$1.24M</td>
<td class="casca-table-num" data-trend="up">+8.2%</td>
</tr>
</tbody>
<tfoot>
<tr><th scope="row">Total</th><td class="casca-table-num">$4.69M</td><td></td></tr>
</tfoot>
</table>
</div>
Classes / attributes:
| Class / attribute | On | Description |
|---|---|---|
.casca-table | <table> | The styled table (border-collapse, zebra, row hover) |
.casca-table-wrap | wrapper <div> | Optional: horizontal scroll on narrow viewports; a bounded block-size makes <thead> sticky |
.casca-table-caption | <caption> | Styled caption |
.casca-table-num | <th> / <td> | Numeric cell: right-aligned + tabular figures |
data-trend="up" | "down" | "flat" | <td> | Trend cell: ▲/▼/– glyph + color. Keep the +/- sign in the text - the sign carries direction without color |
aria-sort="ascending" | "descending" | <th> | Server-driven sort state; shows ↑/↓/↕ glyph on the header link |
.casca-table-select | <th> / <td> | The narrow, centered row-selection (checkbox) column cell |
.casca-row-select | <input type="checkbox"> | A per-row selection checkbox (branded via accent-color); a checked row highlights via :has() |
.casca-row-select-label | <label> | Optional wrapper around .casca-row-select that makes the whole cell the click target (a native <label> forwards clicks - no JS) |
Row selection (no-JS): put a .casca-row-select checkbox in a
.casca-table-select cell, inside a <form>. Checked rows get a brand-tint
highlight (distinct from the gray hover) and the selection submits as
name=value. Give each checkbox a name via aria-labelledby (→ the row header
id) or aria-label; put a .sr-only "Select" label in the column header.
Wrap the checkbox in a .casca-row-select-label so the entire cell (and the
row's height), not just the ~1em native box, is clickable. The label has no
text - the input keeps its accessible name from aria-labelledby / aria-label.
<td class="casca-table-select">
<label class="casca-row-select-label">
<input class="casca-row-select" type="checkbox" name="rows" value="atlas"
aria-labelledby="row-atlas">
</label>
</td>
<th scope="row" id="row-atlas">Atlas Pro</th>
CSS variables: --casca-table-border / -head-bg / -head-color /
-row-stripe / -hover-bg / -foot-bg / -cell-pad-block /
-cell-pad-inline / -trend-up / -trend-down / -row-selected /
-select-accent (+ -dark variants for the color tokens).
Features:
- ✅ Native
<table>semantics - keyboard + screen-reader accessible, no ARIA scaffolding - ✅ Trend is not color-only (▲/▼ glyph + the signed text); forced-colors keeps both, drops only the color
- ✅ Opt-in sticky header, zebra striping, totals footer, row hover
- ✅ Composes with Filter for no-JS row filtering (
data-filteron<tr>) - ✅ No-JS row selection: checked rows highlight (
:has()) and submit with the form; selected wins over zebra + hover; forced-colors uses a systemHighlight. Wrap the checkbox in.casca-row-select-labelto make the full cell the click target - ✅ Dark-mode aware; works from the core build
See site/pages/components/data-table.html.
Range Slider (value input, no-JS)
NEW in v0.11.0
A themed, accessible single-value slider built on the native
<input type="range">. It's a form control: the value submits with the form
and the server re-renders (no JS). Branded with accent-color - the
standardized, cross-browser, no-JS way to color the filled track + thumb.
<div class="casca-range-field">
<label class="casca-range-label" for="threshold">Alert threshold</label>
<input class="casca-range" type="range" id="threshold" name="threshold"
min="0" max="100" value="40" step="5" list="threshold-ticks">
<datalist id="threshold-ticks"> <!-- optional native tick marks -->
<option value="0"></option><option value="50"></option><option value="100"></option>
</datalist>
<div class="casca-range-scale" aria-hidden="true">
<span>0</span><span>50</span><span>100</span>
</div>
</div>
Classes:
| Class | On | Description |
|---|---|---|
.casca-range-field | wrapper <div> | Column layout: label, input, scale |
.casca-range-label | <label> | Field label (associate with for) |
.casca-range | <input type="range"> | The slider; accent-color brands the filled track + thumb |
.casca-range-scale | <div> | Static min/mid/max text row (aria-hidden; the input already announces its range) |
CSS variables: --casca-range-accent (+ -dark) - filled track + thumb
color (default --casca-color-1); --casca-range-block-size - the input
hit area (default 1.5rem).
Features:
- ✅ Native
<input type="range">- keyboard operable (arrows / Home / End), announces name + range + value, no ARIA needed - ✅ Branded fill + thumb via
accent-color(no pseudo-element track replacement, so the no-JS filled-track coloring survives cross-browser) - ✅ Visible focus ring,
:disabledstate, dark-mode + forced-colors aware - ✅ Optional native tick marks via
<datalist>; works from the core build
See site/pages/controls/range-slider.html.
KPI / Stat Card
NEW in v0.12.0
The dashboard scorecard tile that leads a report - a metric label, a big value,
an optional delta (direction + sign, not color-only), and a caption. Pure
layout, no JS. .casca-stat-grid lays out a responsive row of cards.
<dl class="casca-stat-grid">
<div class="casca-stat">
<dt class="casca-stat-label">Monthly revenue</dt>
<dd class="casca-stat-value">$48.2k</dd>
<dd class="casca-stat-delta" data-trend="up">+12.4%</dd>
<dd class="casca-stat-caption">vs. last month</dd>
</div>
<!-- ... more cards ... -->
</dl>
Classes / attributes:
| Class / attribute | On | Description |
|---|---|---|
.casca-stat-grid | wrapper <dl> | Responsive grid (auto-fit, ~12rem min track); definition list groups label-value pairs |
.casca-stat | wrapper <div> (inside <dl>) | The card: bordered tile, vertical stack; groups one stat's <dt> and <dd> children |
.casca-stat-label | <dt> | Metric name (small, muted) - keep it FIRST so AT reads context before the number |
.casca-stat-value | <dd> | The big number (large, bold, tabular) |
.casca-stat-delta | <dd> | Change indicator |
data-trend="up" | "down" | "flat" | .casca-stat-delta | ▲/▼/– glyph + color. Keep the +/- sign in the text - the sign carries direction without color |
.casca-stat-caption | <dd> | Small context line |
.casca-stat-trend | <dd> | Optional sized slot for an inline sparkline / mini chart (drop any casca chart in) |
CSS variables: --casca-stat-bg / -border / -radius / -pad /
-label-color / -value-color / -value-size (default --casca-font-size-5
= 2rem) / -caption-color / -up / -down (+ -dark variants for the colors).
Features:
- ✅ Plain text in reading order - a screen reader announces the card as a sentence; no ARIA needed
- ✅ Delta not color-only (▲/▼ glyph + signed text); forced-colors keeps both, drops only color
- ✅ Responsive grid; collapses to one column on phones
- ✅ Hosts an inline sparkline via
.casca-stat-trend(composition) - ✅ No
:has()/accent-color/color-mix()- renders everywhere; works from the core build
See site/pages/components/stat-card.html.
Keyboard Key (kbd)
NEW in v1.3.0
Style the native <kbd> element inside .casca so a key reads as a key, no
extra classes at the call site. Inline-flex pill with a slightly raised bottom
border (the universal "physical key" signal), monospace glyph, surface
background.
<p>Press <kbd>Ctrl</kbd> + <kbd>K</kbd> to focus search.</p>
<td><kbd>Enter</kbd></td>
Classes: none required. Selector is .casca kbd, so wrapping any subtree
in .casca opts in.
Features:
- ✅ Plain semantic HTML, no extra classes
- ✅ Mono font via
--casca-font-mono, surface + gray tokens, dark and forced-colors aware - ✅ Ships in the core build
See site/pages/components/kbd.html.
Date Field (no-JS)
NEW in v1.3.0
A themed <input type="date">. CSS themes the field (border, focus ring,
padding, font, branded accent); the popup calendar is the browser's own
non-themeable UI, varying by browser/OS. Same trade-off as Range Slider: stay
native, theme what you can, accept the popup. The value submits with the form,
no JavaScript.
<div class="casca-date-field">
<label class="casca-date-label" for="start">Start date</label>
<input class="casca-date" type="date" id="start" name="start" value="2026-05-29">
</div>
Classes:
| Class | On | Description |
|---|---|---|
.casca-date-field | wrapper <div> | Column layout: label + input |
.casca-date-label | <label> | Field label (associate with for) |
.casca-date | <input type="date"> | The themed field; calendar popup is the browser's native UI |
Features:
- ✅ Native form control, keyboard-operable, screen-reader-announced
- ✅ Branded via
accent-color; visible focus ring;:disabledstate - ✅ Dark and forced-colors aware; WebKit picker indicator inverts on dark
- ✅ Ships in
dist/casca.css
The popup calendar UI is browser-native and not styleable. This is the trade-off for staying no-JS; a custom navigable calendar grid would need JavaScript or server round-trips. Firefox exposes fewer pseudo-elements than WebKit and renders its own picker chrome.
See site/pages/controls/date.html.
Color Field (no-JS)
NEW in v1.3.0
A themed <input type="color">. The wrapper takes Casca's border and radius;
the inner swatch fills with the chosen color. The popup picker is the OS-
native picker. Same pattern as Date Field. The chosen color submits with the
form, no JavaScript.
<div class="casca-color-field">
<label class="casca-color-label" for="brand">Brand color</label>
<input class="casca-color" type="color" id="brand" name="brand" value="#1864ab">
</div>
Classes:
| Class | On | Description |
|---|---|---|
.casca-color-field | wrapper <div> | Column layout: label + input |
.casca-color-label | <label> | Field label (associate with for) |
.casca-color | <input type="color"> | The themed field; popup picker is the browser's native UI |
Features:
- ✅ Native form control, keyboard-operable, screen-reader-announced
- ✅ Visible focus ring,
:disabledstate, dark and forced-colors aware - ✅ Ships in
dist/casca.css
Same as Date Field: the popup picker is OS-native and not themeable. A custom HSL-slider color picker would need JavaScript or server round-trips.
See site/pages/controls/color.html.
Interactions (no-JS)
NEW in v0.8.0
@layer casca.interactions - native HTML + :has() patterns that make charts
and lists interactive with zero JavaScript. They require :has() (Chrome
105+, Safari 15.4+, Firefox 121+); without it, content degrades to "everything
visible" and the native controls still work. See the live catalog at
site/pages/index.html, plus the per-interaction pages
(site/pages/controls/series-toggle.html, site/pages/controls/switch.html,
site/pages/controls/disclosure.html, site/pages/controls/filter.html).
Scoping convention: patterns are markup-coupled (the consumer emits exact
markup), keyed on value + data-* attributes, with a scope ancestor for
:has(). Give each radio group (switch) a UNIQUE name so instances on one
page don't cross-wire.
Series Toggle
Check/uncheck a chip to show/hide a chart series. Hidden series use
visibility: hidden (NOT display), so the chart keeps its layout, axes, and
SVG :nth-child coloring/stagger - toggling one never moves or recolors the
others. The toggle controls live inside the same .casca as the series, and
OUTSIDE the chart element. Series carry numeric data-series="1".."8".
<figure class="casca casca-figure">
<fieldset class="casca-toggles">
<legend class="sr-only">Series</legend>
<label class="casca-toggle">
<span class="casca-toggle-box">
<input type="checkbox" class="casca-toggle-input" value="1" checked>
</span>
<span class="casca-toggle-label">
<span class="casca-toggle-swatch" style="--_color: var(--casca-color-1)"></span>
Revenue
</span>
</label>
<!-- ... -->
</fieldset>
<svg ...><polyline data-series="1" .../></svg>
</figure>
| Class | Description |
|---|---|
.casca-toggles | <fieldset> chip row |
.casca-toggle | a <label>: a checkbox cell + a decorator cell, joined as a two-segment control (the box does NOT wrap the checkbox) |
.casca-toggle-box | bordered, padded cell that frames the native checkbox |
.casca-toggle-input | the native checkbox (value="N"); keeps its default appearance |
.casca-toggle-label | the decorator cell (swatch + text); shares a seam with the box |
.casca-toggle-swatch | color dot inside the decorator (--_color) |
Switch (segmented view swap)
A radio group swaps which .casca-switch-view shows (Day/Week/Month, chart
types). Active segment via input:checked + label (no :has() needed); the
view swap uses :has() with a first-view fallback.
Two control skins via data-variant, sharing the same radio + view-swap: the
default segmented pill and "tabs" (an underlined tab strip). For a binary
A/B toggle (with a sliding knob) that composes safely inside other switches, use
the dedicated Segment primitive instead.
<div class="casca-switch">
<fieldset class="casca-switch-controls">
<legend class="sr-only">Range</legend>
<input class="casca-switch-input" type="radio" name="range" id="r1" value="1" checked>
<label class="casca-switch-btn" for="r1">Day</label>
<!-- ... unique `name` per instance ... -->
</fieldset>
<div class="casca-switch-views">
<div class="casca-switch-view" data-view="1">...</div>
</div>
</div>
Disclosure (drill-down)
Styled native <details>/<summary> - open/close is built into HTML, so this
needs no :has() and has no fallback (universal support).
<details class="casca-disclosure">
<summary class="casca-disclosure-summary">Regional breakdown</summary>
<div class="casca-disclosure-body"> ... </div>
</details>
Filter (chips filter a list)
Check categories to show, uncheck to hide matching items. Items collapse
(display: none, so the list reflows). Uses a distinct .casca-filter-input
so it never cross-fires with a series toggle on the same page; items carry
data-filter="1".."8".
<div class="casca-filter">
<fieldset class="casca-toggles">
<label class="casca-toggle">
<span class="casca-toggle-box">
<input type="checkbox" class="casca-filter-input" value="1" checked>
</span>
<span class="casca-toggle-label">
<span class="casca-toggle-swatch" style="--_color: var(--casca-color-1)"></span>
Marketing
</span>
</label>
</fieldset>
<ul><li data-filter="1">...</li></ul>
</div>
Result count + empty-state (no-JS). A display: none item generates no box,
so it does not increment a CSS counter - .casca-filter-count shows the live
count of visible items via counter(). Place it after the items in source
order (counters read document order). .casca-filter-empty shows a message when
no category is checked (requires :has(); without it it stays hidden, so the list
never looks falsely empty). The empty-state is scoped to the filter's own
toggle group, so .casca-toggles and .casca-filter-empty must be direct
children of .casca-filter (as below).
<div class="casca-filter">
<fieldset class="casca-toggles"> ... </fieldset>
<ul><li data-filter="1">...</li></ul>
<p>Showing <span class="casca-filter-count"></span> of 6.</p>
<p class="casca-filter-empty">No items match.</p>
</div>
Nesting. A .casca-filter can contain another .casca-filter (e.g. a page
that filters cards, one of which is itself a live filter). The hide rules, the
count, and the empty-state each compose per filter when every nested filter
uses a value / data-filter namespace disjoint from its ancestors (the
1..8 range; allocate sub-ranges - e.g. outer 1,2,3, inner 4,5). This is
required because :has() cannot be confined to a sub-scope, so an outer filter's
hide rule would otherwise also collapse a nested filter's items that share the
same number. The count stays correct automatically (each .casca-filter resets
its own counter()), and the empty-state stays correct via the direct-child
scoping above.
Segment (binary slide toggle)
A two-state slide toggle for progressive disclosure: a labelled knob that
reveals or hides the optional .casca-segment-extra items in its scope. Anything
NOT marked extra stays visible in both states, so the base set is always
available and the toggle just filters the extras in or out (checked = show all).
Built on the hidden-checkbox + label[for] + subsequent-sibling pattern (the
checkbox comes first; the control and content are its later siblings). No :has(),
so it composes anywhere and re-invalidates reliably on toggle.
<div class="casca-segment">
<input type="checkbox" class="casca-segment-input" id="seg1" checked>
<label class="casca-segment-control" for="seg1">
<span class="casca-segment-label">Core</span>
<span class="casca-segment-track" aria-hidden="true"></span>
<span class="casca-segment-label">Extended</span>
</label>
<ul class="casca-card-grid">
<li><article class="casca-card">always shown</article></li>
<li><article class="casca-card casca-segment-extra">hidden when toggled off</article></li>
</ul>
</div>
All interactions are keyboard-accessible (native checkbox/radio/
<details>), reduced-motion safe, and forced-colors aware. The accessible.casca-datatable always lists every series regardless of what's toggled off.
Modal (overlay dialog, no-JS)
NEW in v1.3.0
A no-JS modal built on the hidden-checkbox + label[for] pattern (same family
as casca-segment, casca-toggle, casca-disclosure). A trigger label opens
it, the close button or a backdrop click close it. Optional slide-in / slide-
out animation configurable via data-slide on the box.
<div class="casca-modal">
<input type="checkbox" id="m1" class="casca-modal-input">
<label class="casca-modal-trigger" for="m1">Open</label>
<label class="casca-modal-backdrop" for="m1" aria-hidden="true"></label>
<aside class="casca-modal-box" data-slide role="dialog" aria-labelledby="m1-t">
<h2 id="m1-t" class="casca-modal-title">Title</h2>
<div class="casca-modal-body">Body</div>
<label class="casca-modal-close" for="m1" aria-label="Close">x</label>
</aside>
</div>
Classes:
| Class | On | Description |
|---|---|---|
.casca-modal | wrapper <div> | Grouping container (no positioning of its own) |
.casca-modal-input | <input type="checkbox"> | The hidden state checkbox; drives open/closed |
.casca-modal-trigger | <label for> | The "open" button (styled label) |
.casca-modal-backdrop | <label for> | Fixed dimmed overlay; clicking it closes |
.casca-modal-box | <aside role="dialog"> | The centered card with title + body + close |
.casca-modal-title | <h2> (or similar) | Title slot |
.casca-modal-body | <div> (or similar) | Body slot; can host any content including forms |
.casca-modal-close | <label for> | The "x" close button |
Attributes:
| Attribute | On | Values | Description |
|---|---|---|---|
data-slide | .casca-modal-box | (bare) / top / bottom / left / right | Slide in from / out to the given direction. Bare attribute defaults to top. Without data-slide, the modal just fades. |
Features:
- ✅ Pure form controls, no JavaScript; degrades to a plain checkbox in ancient engines
- ✅ Backdrop click dismisses (it is a
<label for>covering the viewport) - ✅ Reduced-motion drops the open/close animation; forced-colors keeps the box bounded
- ✅ Ships in
dist/casca.css
Placement contract: The .casca-modal wrapper must sit outside ancestors
that create stacking contexts. Do not nest it inside isolated cards,
transformed panels, opacity effects, z-indexed positioned wrappers, or other
stacking ancestors. casca check-modal <FILE> enforces the statically
detectable cases for authored HTML and narrow modal-related CSS source.
See site/pages/controls/modal.html.
Layout Shell (extended-layout)
The no-JS page/dashboard shell primitives - what you build a dashboard or
gallery FROM, as opposed to the widgets that fill it. Ships in the opt-in
all-in-one dist/casca.css.
Card
A bordered surface with optional .casca-card-title / .casca-card-body /
.casca-card-footer slots. The card owns the frame; you compose a stat, chart,
table, or controls inside. Use a real heading for the title so the document
outline stays correct. All slots are optional - a bare .casca-card is just a
padded surface.
The card carries position: relative and isolation: isolate so consumers
can drop a stretched-link pseudo-element (a.casca-card-title::after { position: absolute; inset: 0 }) directly on top to make the whole tile a
click target, with the link layer and any live controls inside the card
composing in a local stacking context that does not leak outward.
<article class="casca-card">
<h3 class="casca-card-title">Monthly revenue</h3>
<div class="casca-card-body">
<!-- a .casca-stat, a chart, a .casca-table, ... -->
</div>
<footer class="casca-card-footer">Updated 5 minutes ago</footer>
</article>
Card grid
A responsive grid for cards (or any tiles): columns auto-fit at
--casca-card-grid-min and collapse to one column on narrow - intrinsically
responsive, no media/container-query setup required.
<ul class="casca-card-grid">
<li><article class="casca-card"> ... </article></li>
<!-- ... -->
</ul>
Grid items receive a defensive min-inline-size: 0 so a child with inline
width: 100% or oversized intrinsic content shrinks to its grid cell instead
of forcing the cell wider.
Thumb
A clipped, centered tile content area. Use inside a .casca-card to host a
single visual (a chart, an icon, a small composition) without letting a
child's inline width: 100% or oversized intrinsic dimensions burst out of
the tile.
<a class="casca-card" href="...">
<div class="casca-thumb" aria-hidden="true">
<!-- a casca chart, an icon, a small composition -->
</div>
<h3 class="casca-card-title">Bar</h3>
</a>
Variant .casca-thumb-disclosure opens at the top of the tile and caps its
block-size so a <details> expansion does not stretch the surrounding row.
Live-control z-stack: interactive primitives inside a .casca-thumb (a
.casca-toggle, .casca-segment-control, .casca-range, .casca-modal-trigger,
etc.) automatically rise above a stretched-link pseudo-element so they remain
operable when the surrounding tile is a click target.
casca-thumb CSS Variables
| Variable | Default | Purpose |
|---|---|---|
--casca-thumb-block-size | 4rem | Fixed tile content height (min and max) |
Toolbar
A horizontal control bar for filter chips, a segmented switch, or actions. Wraps
by default; data-align distributes groups (between / end),
data-variant="scroll" keeps one row and scrolls on narrow. .casca-toolbar-group
keeps related controls together as one unit.
<menu class="casca-toolbar" data-align="between">
<li class="casca-toolbar-group"><fieldset class="casca-toggles"> ... </fieldset></li>
<li class="casca-toolbar-group"> ...actions... </li>
</menu>
Tabs
Tabs are the existing switch with data-variant="tabs" -
the same radio-driven, no-JS view swap, restyled from a filled pill into an
underlined tab strip. There is no separate .casca-tabs primitive (a tab bar
is a segmented switch; shipping a second one would be redundant).
<div class="casca-switch" data-variant="tabs">
<fieldset class="casca-switch-controls">
<legend class="sr-only">View</legend>
<!-- ...radios + labels... -->
</fieldset>
<div class="casca-switch-views"> ...panels... </div>
</div>
Anchor nav
An in-page #anchor link list (a TOC / section nav), vertical by default or
horizontal with data-orientation="horizontal" (a top-bar TOC; the active marker
becomes an underline instead of a leading rail). Optionally sticky (data-sticky).
The active item is marked by the author/server with aria-current
(true / page / location) - pure CSS cannot derive the
scrolled-to section as the link's own state, so active state rides on
aria-current (which a screen reader also announces) and degrades to a plain
link list.
<nav class="casca-anchor-nav" aria-label="On this page" data-sticky>
<a class="casca-anchor-nav-link" href="#charts" aria-current="true">Charts</a>
<a class="casca-anchor-nav-link" href="#tables">Tables</a>
</nav>
Layout primitives are
.casca-*-scoped, dark-mode + forced-colors aware, and add no:rootglobals. They ship indist/casca.css.
CSS Custom Properties
Global Design Tokens
Colors
/* Grayscale */
--casca-gray-0: #ffffff;
--casca-gray-1: #f8f9fa;
--casca-gray-2: #e9ecef;
--casca-gray-3: #dee2e6;
--casca-gray-4: #ced4da;
--casca-gray-5: #adb5bd;
--casca-gray-6: #6c757d;
--casca-gray-7: #495057;
--casca-gray-8: #343a40;
--casca-gray-9: #212529;
/* Chart colors */
--casca-blue-5: #4dabf7;
--casca-blue-6: #228be6;
--casca-teal-5: #20c997;
--casca-teal-6: #12b886;
--casca-orange-5: #ff922b;
--casca-orange-6: #fd7e14;
--casca-pink-5: #f06595;
--casca-pink-6: #e64980;
--casca-green-5: #51cf66;
--casca-green-6: #40c057;
--casca-purple-5: #9775fa;
--casca-purple-6: #845ef7;
--casca-yellow-5: #ffd43b;
--casca-yellow-6: #fab005;
--casca-red-5: #ff6b6b;
--casca-red-6: #fa5252;
Spacing
--casca-size-1: 0.25rem; /* 4px */
--casca-size-2: 0.5rem; /* 8px */
--casca-size-3: 0.75rem; /* 12px */
--casca-size-4: 1rem; /* 16px */
--casca-size-5: 1.5rem; /* 24px */
--casca-size-6: 2rem; /* 32px */
Typography
--casca-font-size-00: 0.75rem; /* 12px */
--casca-font-size-0: 0.875rem; /* 14px */
--casca-font-size-1: 1rem; /* 16px */
--casca-font-size-3: 1.25rem; /* 20px */
--casca-font-size-4: 1.5rem; /* 24px */
--casca-font-size-5: 2rem; /* 32px */
--casca-font-weight-4: 400; /* normal */
--casca-font-weight-6: 600; /* semibold */
--casca-font-weight-7: 700; /* bold */
--casca-font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
Z-Ladder Tokens
Use these tokens for every z-index declaration. auto is also accepted when
the element should not participate in a named layer.
--casca-z-base
--casca-z-local-1
--casca-z-local-2
--casca-z-local-3
--casca-z-local-4
--casca-z-sticky
--casca-z-overlay
--casca-z-popover
--casca-z-modal
casca check-z-ladder <FILE> rejects bare numeric z-index values, unknown
ladder tokens, CSS-wide keywords, var() fallbacks, and calc() expressions.
Chart-Specific Tokens
/* General */
--casca-height: 300px;
--casca-width: 100%;
--casca-gap: 0.5rem;
--casca-padding: 1rem;
/* Bars */
--casca-bar-width: 2rem;
--casca-bar-radius: 0.25rem;
--casca-bar-min-height: 2px;
/* Pies */
--casca-pie-size: 250px;
--casca-donut-hole-size: 40%;
/* Lines */
--casca-line-stroke-width: 2px;
--casca-line-point-size: 6px;
--casca-line-area-opacity: 0.1;
/* Progress & Gauges */
--casca-progress-height: 0.75rem;
--casca-progress-radius: 0.25rem;
--casca-gauge-size: 200px;
--casca-gauge-thickness: 20px;
/* Animation */
--casca-duration: 0.4s;
--casca-easing: cubic-bezier(0.2, 0, 0, 1);
/* Colors */
--casca-label-color: #6c757d;
--casca-axis-color: #ced4da;
--casca-grid-color: #e9ecef;
--casca-axis-width: 1px;
/* Axis-label primitive (.casca-axis, .casca-plot) */
--casca-axis-y-width: 2.25rem; /* left tick gutter in .casca-plot */
--casca-axis-x-height: 1.25rem; /* bottom tick gutter in .casca-plot */
--casca-axis-count: 6; /* radial spoke count (set per radar) */
--casca-axis-radius: 56%; /* radial label distance from center */
/* Chart palette */
--casca-color-1: var(--casca-blue-6);
--casca-color-2: var(--casca-teal-6);
--casca-color-3: var(--casca-orange-6);
--casca-color-4: var(--casca-pink-6);
--casca-color-5: var(--casca-green-6);
--casca-color-6: var(--casca-purple-6);
--casca-color-7: var(--casca-yellow-6);
--casca-color-8: var(--casca-red-6);
/* Legible foreground for text/icons sitting ON the color-1 accent fill
(e.g. the active switch segment, a selected slot). Defaults to white; the
themed bundles flip it to a dark value in dark mode, where color-1 remaps to
a lighter tint. If you override color-1, set on-accent to a color that meets
WCAG AA against your accent in both light and dark. */
--casca-on-accent: var(--casca-gray-0);
/* Layout shell (extended-layout) */
--casca-card-bg: var(--casca-surface-1);
--casca-card-border: var(--casca-gray-3); /* -dark: --casca-gray-7 */
--casca-card-radius: var(--casca-radius-2);
--casca-card-pad: var(--casca-size-5);
--casca-card-gap: var(--casca-size-3);
--casca-card-title-color: var(--casca-gray-9); /* -dark: --casca-gray-0 */
--casca-card-title-size: var(--casca-font-size-1);
--casca-card-footer-color: var(--casca-gray-6); /* -dark: --casca-gray-5 */
--casca-card-grid-min: 16rem; /* card-grid min column width */
--casca-card-grid-gap: var(--casca-size-4);
--casca-toolbar-gap: var(--casca-size-3);
--casca-anchor-nav-gap: var(--casca-size-1);
--casca-anchor-nav-color: var(--casca-gray-7); /* -dark: --casca-gray-4 */
--casca-anchor-nav-active: var(--casca-color-1);
--casca-anchor-nav-pad: var(--casca-size-2);
Customization Example
/* Override default colors */
:root {
--casca-color-1: #0066cc;
--casca-color-2: #00aa66;
--casca-bar-radius: 0.5rem;
--casca-duration: 0.6s;
}
/* Chart-specific customization */
.casca.custom-chart {
--casca-height: 400px;
--casca-bar-width: 3rem;
}
Accessibility Classes
/* Screen reader only content */
.casca-data { /* Visually hidden data table */ }
.sr-only { /* Screen reader only */ }
/* Live regions for updates */
.casca-live-region { /* Announces changes */ }
Responsive Breakpoints
Casca uses container queries for responsive behavior:
/* Small charts (< 400px) */
@container casca (max-width: 400px) {
.casca-bar-value {
--_bar-width: 1rem; /* Narrower bars */
}
.casca-bar-label {
font-size: 0.75rem;
}
}
/* Tiny charts (< 300px) */
@container casca (max-width: 300px) {
.casca-pie {
--_pie-size: 150px;
}
}
Browser Support
Minimum Requirements:
- Chrome 105+ (Container Queries, :has())
- Firefox 110+ (Container Queries)
- Safari 16+ (Container Queries, :has())
- Edge 105+
Progressive Enhancement:
- Falls back gracefully in older browsers
- Core data remains accessible via tables
- No JavaScript required for any functionality
CLI Theme Tools
casca build
casca build composes the Casca CSS bundle from the embedded
src/casca.css entry. With no flags it emits the all-in-one bundle
(default themes + picker on, matching the committed dist/casca.css).
casca build \
[--themes solar[,cyber][,lunar][,arcade]] \
[--theme-manifest <path>] \
[--picker on|off] \
[--picker-root <selector>] \
[--output <path>] \
[--minify on|off] \
[--targets <browserslist-string>] \
[--source-dir <path>]
--themes defaults to all four moods: solar, cyber, lunar, and
arcade. With picker mode on, solar is the unscoped base by default,
while cyber, lunar, and arcade are scoped through
body[data-casca-theme="<mood>"] and
body:has(.casca-theme-picker[data-casca-theme-picker] input[name="casca-theme"][value="<mood>"]:checked).
A single theme with --picker on is allowed and warns because there is no
overlay to switch to.
--theme-manifest <path> reads a TOML theme manifest. The manifest
schema is:
[theme_state]
param = "casca-theme"
cookie = "casca-theme"
picker_root = ".casca-theme-picker[data-casca-theme-picker]"
setter_path = "/_casca/theme"
default = "solar"
[[themes]]
id = "solar"
label = "Solar"
kind = "css" # or "tokens"
source = "../src/themes/default.css"
base = true
swatch_light = ["#f2ebda", "#c8633a"]
swatch_dark = ["#251e16", "#e68a60"]
Theme ids must match [a-z0-9][a-z0-9-]{0,47}. kind = "css" runs through
the scoped-selector validator. kind = "tokens" compiles a TOML token file
with [light], [dark], [roles.light], and [roles.dark] tables before
validation. In manifest mode, --themes accepts manifest theme ids instead
of the four built-in mood ids. If --themes is omitted, all manifest
themes are emitted in manifest order with the manifest base first. Picker
scoping uses the manifest picker_root.
--picker off appends each requested theme in order without picker
scoping. Multiple themes are allowed in that mode and warn because later
themes override earlier themes through normal cascade order.
--output - writes CSS to stdout. Any warnings are still written to
stderr. --source-dir <path> reads from a Casca-shaped source tree
instead of embedded sources; pass either the src/ directory or a
repository root containing src/.
casca dev and casca preview theme serving
When a project config contains a theme-picker widget, casca dev and
casca preview load the referenced manifest once at startup and serve
GET /_casca/theme from that in-memory context. casca dev also routes
changes to the manifest file through its existing config rebuild path, so a
successful rebuild swaps the output state and theme serving context together.
For portal HTML responses, both commands resolve the active mood from query,
cookie, then manifest default. They set body[data-casca-theme="<id>"], update
the checked picker radio, refresh the hidden return_to, and emit
Vary: Cookie. Assets, redirects, feeds, /_casca/ws, /_casca/dev.js, the
setter path, and other reserved /_casca/ paths bypass the transform.
If no manifest is configured, the endpoint is not active and the commands keep their static serving behavior.
casca lint --theme-overlay
casca lint --theme-overlay validates a theme source file before it is
compiled. It checks the Casca theme contract: allowed at-rules, no nested
style rules, custom properties in the --casca-* or --_ namespace, and
the selected base or overlay role.
casca lint --theme-overlay <FILE> --as base|overlay [--base <FILE>]
--as base validates the file as a base theme.
--as overlay validates the file as an overlay. --base <FILE> may be
supplied for base-relative checks, including public variable names and
matching light or dark contexts.
Exit code 0 means validation is clean. Exit code 1 means validation or
parsing failed. Exit code 2 means the command line did not match the clap
schema.
In theme-overlay mode, positional FILE arguments are rejected. Pass the
theme source path through --theme-overlay. This preserves the legacy
casca lint <file> dispatch for compiled CSS bundle linting.
CLI Lint Checks
casca check-z-ladder
casca check-z-ladder <FILE> scans one CSS file and reports every z-index
declaration that does not use auto or a declared Casca z-ladder token.
casca check-z-ladder <FILE>
Accepted values are auto and var(--casca-z-base),
var(--casca-z-local-1), var(--casca-z-local-2),
var(--casca-z-local-3), var(--casca-z-local-4),
var(--casca-z-sticky), var(--casca-z-overlay),
var(--casca-z-popover), and var(--casca-z-modal).
Diagnostics use <file>:<line>:<col> and point at the z-index
declaration. Exit code 0 means clean, 1 means violations or a read error,
and 2 is reserved for command-line usage errors.
casca check-modal
casca check-modal <FILE> checks modal DOM placement and narrow modal-related
CSS regressions.
casca check-modal <FILE>
.html and .htm inputs are tokenized and every .casca-modal ancestor is
checked for known stacking-context triggers. .css inputs check direct
.casca-modal wrapper stacking rules, mapped blocker-class drift, and bounded
selector-ancestor risks using descendant and child combinators. Other
extensions return exit code 2.
Diagnostics use <file>:<line>:<col> and identify the offending ancestor or
selector. Exit code 0 means clean, 1 means violations or a read error, and
2 means the input extension or command line is unsupported.
Version History
The release history lives in CHANGELOG.md - the single source of truth. (This section used to duplicate it and drifted out of date, so it now just points there.)
Linked references
- Cascalink
- linked as API
- Casca Adoption Guidelink
- linked as API Reference