variable-vis

Encode a signed continuous score on any div or span with CSS alone — subtle enough to stay in the periphery while reading, with no JS required at render time.

Quick start

Load the stylesheet:

<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/mimno/TextVariableVis@main/variable-vis.css">

Add the base class and a value, in whatever units your data naturally comes in:

<div class="tvv tvv-edge tvv-dot" style="--tvv-value: -0.6"> I have measured out my life with coffee spoons. </div>

If your dataset's raw values aren't already on a −1..1 scale, set --tvv-scale once on a shared container to the largest magnitude in your data — every element underneath normalizes against it automatically, so the picture looks the same no matter what units the scores are in:

<!-- a dataset whose scores run roughly -100..100 --> <div class="poem" style="--tvv-scale: 100"> <div class="tvv tvv-edge tvv-dot" style="--tvv-value: -60">...</div> <div class="tvv tvv-edge tvv-dot" style="--tvv-value: 90">...</div> </div> // CSS can't scan your dataset to find its own max value - if you // don't know it ahead of time, compute it once in JS before you // render the markup above. Both lines below are things YOU provide: // `scores` is whatever array of raw numbers you're about to render, // and `container` is the element you put --tvv-scale on (the same // one wrapping the .tvv elements, e.g. the .poem div above). const scores = [-60, 90, 12, -8, 45]; // e.g. one per line you're about to draw const container = document.querySelector('.poem'); container.style.setProperty('--tvv-scale', Math.max(...scores.map(Math.abs)));

Try it

Pick one option from each of the three slots below (fill / edge / margin each occupy a single CSS background or pseudo-element, so only one per group can be active at a time), plus any free modifiers, and copy the resulting markup.

Fill (background)
Edge (::after)
Margin (::before)
Free modifiers (combine freely)
Reveal mode
I have measured out my life with coffee spoons.−0.90
April is the cruellest month, breeding lilacs.−0.40
And miles to go before I sleep.0.00
Hope is the thing with feathers.+0.45
Shall I compare thee to a summer's day?+0.90

How it works

Slots (pick at most one per group)

SlotCSS usedClasses
Fillbackgroundtvv-wash, tvv-texture, tvv-corner
Edge::aftertvv-edge, tvv-center
Margin::beforetvv-gutter, tvv-dot

An element only has one background, one ::before, and one ::after — that's a hard CSS limit, not a stylistic choice, so each slot can hold exactly one active class.

Free modifiers (combine with anything)

ClassWhat it does
tvv-glowInset box-shadow bleeding in from the side matching sign
tvv-inkTiny opacity/lightness nudge to the text color itself
tvv-weightShifts font-weight / variable-font wght axis
tvv-shiftA few px of horizontal translation — works with no color at all
tvv-radiusAsymmetric corner rounding, sign-dependent

Scale invariance

Every effect is driven by --tvv-value divided by --tvv-scale, normalized to the −1..1 range before anything else happens. CSS can't inspect sibling elements to find a dataset's own range automatically, so --tvv-scale is the one number you supply — set it on a container, not per element. Scaling an entire dataset by a constant and updating --tvv-scale by the same factor reproduces an identical picture.

Accessibility

Color-coded methods (tvv-wash, tvv-gutter, tvv-corner, tvv-glow, tvv-ink) are invisible to some color-vision-deficient readers. Pair at least one position/shape-based method (tvv-edge, tvv-center, tvv-shift, tvv-dot, tvv-radius) with any color method so the meaning survives even if hue doesn't.

Reveal modes

Toggle a class on an ancestor (e.g. <body class="tvv-mode-subtle">) to globally scale intensity without touching individual elements: tvv-mode-off, tvv-mode-subtle (default), tvv-mode-explicit. There's also a built-in :hover boost for an on-demand reveal.