Graphics Press CSS
A Tufte-inspired typography and data visualization CSS library
Graphics Press CSS encodes the design principles of Edward Tufte's five
books into reusable CSS components. Every element can be traced to a specific passage
or figure. The library requires no JavaScript, supports automatic dark mode via
prefers-color-scheme, and ships as plain CSS, an npm package, or a
Tailwind CSS plugin.
Installation
CDN (quickstart)
Drop a single <link> tag into your HTML:
<link rel="stylesheet" href="https://unpkg.com/@andrewxhill/[email protected]/css/graphics-press.css">
npm (raw CSS)
npm install @andrewxhill/graphics-press-css
Then either import in CSS:
@import '@andrewxhill/graphics-press-css';
Or link in HTML:
<link rel="stylesheet" href="node_modules/@andrewxhill/graphics-press-css/css/graphics-press.css">
Tailwind CSS plugin
npm install @andrewxhill/graphics-press-css tailwindcss
In tailwind.config.js:
module.exports = {
plugins: [
require('@andrewxhill/graphics-press-css/tailwind'),
],
}
Next.js font variation
For a sans-driven application mode, load Inter and JetBrains Mono with next/font/google and opt into the variation with .gp-font-inter or data-gp-font="inter".
import { Inter, JetBrains_Mono } from "next/font/google";
const inter = Inter({
subsets: ["latin"],
variable: "--font-inter",
display: "swap",
});
const jetbrainsMono = JetBrains_Mono({
subsets: ["latin"],
variable: "--font-jetbrains-mono",
display: "swap",
});
export default function RootLayout({ children }) {
return (
<html
lang="en"
className={`${inter.variable} ${jetbrainsMono.variable} gp-font-inter`}
>
<body>{children}</body>
</html>
);
}
No external font <link> tags are required.
Quick Start
The minimal HTML structure to use the library:
<!DOCTYPE html>
<html lang="en" class="light">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://unpkg.com/@andrewxhill/[email protected]/css/graphics-press.css">
</head>
<body>
<article>
<h1>Your Title</h1>
<p class="subtitle">Your subtitle here</p>
<section>
<h2>Section heading</h2>
<p>Your content...</p>
</section>
</article>
</body>
</html>
Dark Mode
Dark mode is supported three ways:
Automatic
The library responds to prefers-color-scheme: dark automatically.
No classes needed — just ship the CSS.
Manual toggle via classes
Add .dark or .light to <html>:
<html class="dark"> <!-- force dark -->
<html class="light"> <!-- force light -->
JavaScript toggle
const html = document.documentElement;
let dark = false;
function setTheme(d) {
dark = d;
html.classList.toggle('dark', dark);
html.classList.toggle('light', !dark);
}
document.getElementById('themeToggle')
.addEventListener('click', () => setTheme(!dark));
Component Showcase
Every component below is a live example styled by the library itself. Click "Show code" to see the HTML markup pattern.
Dot Chart
"A table is nearly always better than a dumb pie chart; the dot chart is nearly always better than a dumb bar chart." — The Visual Display of Quantitative Information, p. 178
Life expectancy at birth by country, 2022. Scale: 50–90 years.
World Health Organization, World Health Statistics 2023.
Dumbbell Chart
Renewable electricity share, 2010 vs 2023 (%). Each item shows start, end, and magnitude of change simultaneously.
IEA World Energy Statistics 2024.
Strip Plot
Daily high temperatures in four cities, July 2023. Each dot is one day. The strip plot shows all observations — no invented distribution shape.
NOAA Daily Climate Report. n=12 per city shown.
Stem-and-Leaf
"The stem-and-leaf plot constructs the distribution of a variable with the numbers themselves." — The Visual Display of Quantitative Information, p. 140
Marathon finish times. Left = Women, Right = Men. Each digit is one runner's tens digit of minutes past the listed hour.
| Women | h | Men |
|---|---|---|
| 3 | 2 4 7 8 | |
| 8 5 3 1 | 4 | 0 1 2 3 5 6 8 9 |
| 9 8 7 6 4 4 3 2 1 0 | 5 | 0 1 2 3 4 6 7 8 |
| 9 8 8 7 5 4 3 2 1 1 | 6 | 0 2 3 5 6 8 |
| 8 7 6 5 5 3 2 | 7 | 1 4 5 8 |
| 9 7 4 2 | 8 | 2 6 |
| 5 3 | 9 | 4 |
h = hour · leaf = tens digit of minutes · stem 5, leaf 3 = 53 min
Bullet Graph
Tufte's replacement for gauge and speedometer charts. Shows current value, target, and qualitative ranges in a compact horizontal form. — Beautiful Evidence, p. 176
Bar = actual. Thin tick = target. Gray bands = performance ranges.
Sparkline Table
"The point of sparklines is to be small enough to embed in text or tables, with enough resolution to show trends and variation at a glance." — Beautiful Evidence, pp. 47-63
| Product | Trend | Last | Min | Max | Change |
|---|---|---|---|---|---|
| Core API | 1,840 | 720 | 1,840 | +155% | |
| Dashboard | 842 | 604 | 842 | +39% | |
| Mobile SDK | 223 | 223 | 512 | -56% | |
| Analytics | 1,104 | 890 | 1,104 | +24% |
Min Last
Heat Table
Mean monthly temperature by city. Cell color encodes magnitude. Toggle dark mode to see the palette adapt.
| City | Jan | Feb | Mar | Apr | May | Jun | Jul | Aug | Sep | Oct | Nov | Dec |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Bangkok | 27 | 29 | 31 | 33 | 32 | 30 | 29 | 29 | 29 | 28 | 28 | 27 |
| London | 5 | 6 | 8 | 11 | 14 | 17 | 19 | 19 | 16 | 12 | 8 | 6 |
| New York | 1 | 2 | 7 | 13 | 18 | 23 | 26 | 25 | 21 | 15 | 9 | 3 |
| Sydney | 28 | 28 | 25 | 21 | 17 | 14 | 13 | 14 | 17 | 20 | 23 | 26 |
| Reykjavik | -1 | -1 | 1 | 4 | 8 | 11 | 12 | 12 | 9 | 6 | 2 | 0 |
Ranked Table
Dotted leaders guide the eye from label to value. Rank numbers are recessive.
| # | Language | Share | YoY |
|---|---|---|---|
| 1 | JavaScript | 65.8% | -0.4pp |
| 2 | Python | 51.2% | +1.8pp |
| 3 | TypeScript | 43.9% | +3.1pp |
| 4 | Rust | 13.1% | +2.2pp |
| 5 | Go | 12.7% | +0.9pp |
| 6 | Java | 30.6% | -2.4pp |
Rust highlighted: fastest-growing in absolute percentage points.
Stack Overflow Developer Survey 2024.
Interactive Chart — Hover to Isolate Series
Evidence Layout
Image and analysis belong together. — Beautiful Evidence, pp. 82-97
The trajectory is unambiguous. From 1950 to 1980, anomalies remained within the pre-industrial reference band. After 1980 the trend accelerates sharply.
The 1.2°C anomaly reached in 2020 is extreme not by geological standards, but by the scale of human civilization. Every major agricultural system was designed for a climate that no longer exists.
The rate matters as much as the level. Roughly 0.2°C per decade since 1980 — faster than any rate in the 800,000-year ice core record.
Evidence layout from Beautiful Evidence: image and analysis as a unified analytical object. No page-turning between chart and interpretation.
Timeline
AI milestones, 2017-2024. Periods as colored bars, events as dots on the baseline.
Small Multiples
"At the heart of quantitative reasoning is a single question: Compared to what?" — Envisioning Information, p. 67
■ Solar ■ Wind capacity growth 2000-2023 · same scale all panels
Slopegraph
Invented by Tufte in The Visual Display of Quantitative Information, p. 158. Two time points, one line per entity. The slope is the data.
Government expenditure as % of GDP, 1995 and 2023. Hover any slope to inspect.
Government expenditure as % of GDP, selected countries.
Parallel Coordinates
Shows many variables simultaneously for many observations. Each vertical axis is one variable; each line crosses all axes at its values. Denmark (red) leads across all five welfare dimensions.
UN Human Development Report 2023; Reporters Without Borders 2023. Axes normalized 0-1.
Minimal Bar Chart
The default variant shows only a leader line and endpoint tick. The filled variant is available when visual weight aids legibility.
Endpoint tick only (default)
Filled variant
Pull Statistics & Stat Grid
When a single number is the finding, treat it as a finding. Large italic type, Gill Sans label beneath.
Ghosting & Emphasis
Tufte's "smallest effective difference": reduce context data to near-invisible
gray so the focal data pops.
.ghost · .near-ghost · .focal
opacity: 0.22 via .ghost.
Focal country at full ink. The eye goes immediately to the argument.Figure Compare & Before/After Columns
Two layout components for side-by-side comparisons.
Bullet points fragment continuous reasoning into disconnected fragments. Each item competes equally for attention regardless of importance.
Prose allows the argument to unfold in sequence. Relationships between ideas are expressed by the structure of the sentences, not by the structure of the slide.
Margin Notes & Margin Figures
The right margin carries three kinds of content.
Numbered sidenotes
Sidenotes appear beside the text they annotate.
This note uses position: sticky; top: 2rem — it stays visible
while you scroll through the section.
appear with a superscript. Unnumbered margin notes
Margin note
A free commentary in the margin. Cross-references, glosses, small figures.
No number, no anchor — it floats beside the relevant text.
are free glosses. Margin figures
A small chart in the margin. Spatial adjacency does the work.
place small charts directly beside the text that references them.
On narrow screens, the ⊕ button reveals them inline.
Datum & Data-Note
The .datum class treats inline numbers with slightly more
typographic attention — lining figures, Gill Sans, tight tracking —
making them visually distinct without disrupting the sentence.
In 2023, global renewable electricity capacity reached 3,382 GW, up from 2,802 GW in 2021 — a +20.7% increase in two years. The cost of utility-scale solar fell from $0.38/kWh in 2010 to $0.049/kWh in 2023, a -87% reduction.
Source & methodology
Capacity figures: IRENA Renewable Capacity Statistics 2024. Cost figures: IRENA Renewable Power Generation Costs 2023. All figures are global aggregates; regional variation is substantial. The cost series uses weighted average levelized cost of energy (LCOE) for utility-scale solar PV, 2010 constant USD.
Slippy Map
Good basemaps should recede; the argument should sit on top. A hosted vector style plus a few direct analytical marks is the cleanest public example for this library.
This example uses MapLibre GL JS on GitHub Pages with OpenFreeMap’s Liberty style. The base map is familiar and free, attribution is handled automatically by MapLibre, and the overlay remains plain GeoJSON you can inspect and replace.
Six metro areas, one corridor, and a restrained analytical overlay
Circle area encodes illustrative annual passengers. The ochre line isolates the Boston-Washington corridor so the basemap stays contextual instead of argumentative.
- Northeast megaregion
- Interior hubs
- Highlighted corridor
Basemap: OpenFreeMap Liberty, derived from OpenStreetMap/OpenMapTiles. Overlay data is illustrative only and intended to demonstrate symbol hierarchy, labeling, and annotation.
Why this stack
OpenFreeMap publishes a no-key hosted style URL for MapLibre, including labels and attribution. That makes it a better docs default than pointing a public demo at tile.openstreetmap.org, whose usage policy explicitly says the standard tile service is limited and should be avoided if you cannot meet its requirements or need another OSM-derived service.
Strategy Brief
Review note from a live client: the typography is already doing good work, but the KPI row, tab strip, analytic cards, and timeline bars want their own system instead of bespoke app CSS.
Merge Machine Alpha
A compact briefing shell for strategy dashboards: terse provenance up top, one dominant number, then a disciplined sequence of metrics, cards, and timelines beneath.
Main (0x6031...f96d)
$71.58Msb_a (0xa45f...)
$11.79Msb_b (0xddda...)
$3.33Msb_c (0x5b49...)
$6.81MAlgorithm sketch
every hour:
scan active prediction markets
filter to paired YES/NO liquidity
select the 20 newest markets
post makers on both sides
fill for 12 minutes
roll to next market pair
Illustrative strategy-brief numbers modeled after a live Recall Network client layout. Values here are synthetic and intended to demonstrate typographic structure rather than document a real trading strategy.
Analytics App UI
Some client work needs more than prose blocks: filters, search, state messaging, KPI tiles, and dense tables. The app layer keeps those pieces readable without dropping into generic SaaS styling.
Readable by default
Scanner pass: 61%
Representative Positions
| Wallet | Status | PnL | Maker % | Volume |
|---|---|---|---|---|
| 0x6031...f96d | Live | $71.6M | 91% | $44.8M |
| 0xa45f...39b1 | Kalshi | $11.8M | 87% | $21.3M |
| 0x6916...0e11 | Derived | $7.3M | 74% | $9.5M |
Use .gp-meter for quantitative table cells like maker share, completion rate, balance ratio, or other compact progress-style values.
Workspace Shell
Top-level analytics routes usually want a wider frame and a different header balance than dossier/detail pages. Keep that behavior in the library instead of app-local layout CSS.
Prediction Markets Explorer [Beta]
Chart Interop
Use semantic chart variables instead of app-local hex palettes. The same tokens can drive D3, Recharts, ECharts, or plain SVG without diverging from the typographic system.
Use semantic outcomes first
Shared hues for library and app charts
const styles = getComputedStyle(document.documentElement);
const positive = styles.getPropertyValue('--gp-chart-positive').trim();
const negative = styles.getPropertyValue('--gp-chart-negative').trim();
const axis = styles.getPropertyValue('--gp-chart-axis').trim();
const grid = styles.getPropertyValue('--gp-chart-grid').trim();
Editorial Spread
Investigative pages often need narrow paired columns for prose and metadata, followed by a chart or timeline that spans both. The spread layout makes that explicit instead of forcing every app to hand-roll breakpoint math.
Left Column
Keep prose narrow and aligned. This is the reading column for operational narrative.
Right Column
This column can carry the side-analysis without collapsing into card spam.
Wide Breakout
<div class="gp-spread">
<div class="gp-spread__col">...</div>
<div class="gp-spread__col">...</div>
<div class="gp-spread__wide">
<div class="gp-chart-frame">...</div>
</div>
</div>
Treemap Framing
Treemaps in research apps usually need more than bare rectangles. Make the hierarchy loud and the decoration quiet: strong category frames, explicit legend and crumbs, muted micro-cells, and only enough venue color to separate meaning without shimmer.
Use category blocks and header bands to orient the reader before asking them to parse any individual tile.
Tiny rectangles should recede. Heavy borders and loud color on every cell create shimmer and visual fatigue.
Reserve stronger color for actual semantic distinctions such as venue or status. Do not let every stroke compete equally.
Politics — 482 markets
US Elections
Will the Democratic nominee win Arizona?
Let category structure do most of the work. Use restrained box borders, a clearer top band, and a short legend.
Don’t round or saturate everything. Rounded section blocks and bright leaf borders make a treemap feel like generic app chrome instead of analytical evidence.
Mute micro-cells. Dense fields should read as texture until the user hovers or zooms.
Don’t give tiny tiles equal visual weight. If every small mark has a strong outline, the whole figure starts vibrating.
<div class="gp-treemap__legend">
<div class="gp-treemap__legend-group">...</div>
<span class="gp-treemap__legend-note">...</span>
</div>
<div class="gp-treemap__crumbs">...</div>
<div class="gp-treemap">
<div class="gp-treemap__header">...</div>
<div class="gp-treemap__body">
<svg>...</svg>
</div>
</div>
<div class="gp-treemap__tooltip">...</div>
Analytics Application Shell
When using tufte2 for a real dashboard or research tool, keep the workspace wide and the interpretation narrow. The top frame should handle tabs, KPIs, and filters; individual figures should still read like evidence, not like toy widgets.
Workspace Header
The app shell can be wide. The actual argument still lives inside specific figures, tables, and briefings. Use the shell to orient, not to decorate.
| Wallet | Share | PnL |
|---|---|---|
| High Conviction 1 | 78% | +$184k |
| Macro Rotation | 44% | +$71k |
Use .gp-meter for compact quantitative cells and keep the number visible. The bar is support, not the whole message.
Separate shell from evidence. Tabs, KPIs, and filters belong in the workspace header; charts and tables should still read like editorial figures.
Don’t card-ify everything. If every view becomes a rounded widget, the app loses the calm analytical rhythm that makes dense information readable.
Use semantic chart tokens everywhere. Positive, negative, accent, axis, grid, and surface should all come from shared library variables.
Don’t invent route-by-route palettes. Treemaps, bars, tables, and timelines should all speak the same color language.
<div class="gp-analytics-shell">
<div class="gp-analytics-shell__top">...</div>
<p class="gp-analytics-shell__note">...</p>
<div class="gp-analytics-shell__layout">
<div><div class="gp-chart-frame">...</div></div>
<div><table class="gp-data-table">...</table></div>
<div class="gp-analytics-shell__wide">...</div>
</div>
</div>
Ranked Bars
Category tallies, market-share ladders, and top-N bar views should read like ranked evidence, not like a stack of random cards. Use a shared ranked-bar shell so bar tabs in analytics apps stay aligned with the rest of the system.
<div class="gp-rank-list">
<article class="gp-rank-card">
<div class="gp-rank-card__header">...</div>
<div class="gp-stack-bar">...</div>
<div class="gp-rank-list gp-rank-list--nested">
<div class="gp-rank-row">...</div>
</div>
</article>
</div>
CSS Custom Properties Reference
All design tokens are CSS custom properties. Override any at :root to retheme
without forking the library.
Layout
| Property | Default | Description |
|---|---|---|
--gp-text-width | 640px | Reading column width |
--gp-margin-width | 260px | Sidenote/margin column |
--gp-gutter | 48px | Gap between text and margin |
--gp-outer-margin | clamp(3rem, 8vw, 6rem) | Fluid outer margin |
Typefaces
| Property | Default |
|---|---|
--gp-serif | ET Book, Palatino Linotype, Palatino, Georgia, serif |
--gp-sans | Gill Sans, Gill Sans MT, Calibri, Optima, sans-serif |
--gp-mono | Lucida Console, Andale Mono, Monaco, Consolas, monospace |
Type Scale
| Property | Default |
|---|---|
--gp-font-size | clamp(14px, 1vw + 10px, 16px) |
--gp-line-height | clamp(1.58, 1.5 + 0.4vw, 1.68) |
--gp-small | 0.8em |
--gp-caption | 0.77em |
--gp-micro | 0.69em |
Spacing
| Property | Default |
|---|---|
--gp-space-xs | 0.25rem |
--gp-space-sm | 0.5rem |
--gp-space-md | 1rem |
--gp-space-lg | 2rem |
--gp-space-xl | 4rem |
Surface & Ink
| Property | Light | Role |
|---|---|---|
--gp-paper | oklch(99.4% 0.008 98) | Background |
--gp-ink | oklch(17% 0.004 60) | Body text |
--gp-margin-ink | oklch(43% 0.003 60) | Secondary text |
--gp-ghost-ink | oklch(69% 0.003 60) | Tertiary text |
--gp-rule | oklch(62% 0.002 60) | Rules, borders |
--gp-light-rule | oklch(84% 0.005 95) | Light separators |
--gp-code-bg | oklch(95% 0.006 95) | Code background |
Categorical Palette (equal perceptual lightness)
| Property | Value | Swatch |
|---|---|---|
--gp-red | oklch(44% 0.14 27) | |
--gp-blue | oklch(44% 0.09 251) | |
--gp-green | oklch(50% 0.08 149) | |
--gp-ochre | oklch(49% 0.10 72) | |
--gp-brown | oklch(42% 0.09 52) | |
--gp-gray-data | oklch(51% 0.002 60) |
Sequential & Diverging Ramps
| Property | Value |
|---|---|
--gp-seq-1 through --gp-seq-5 | Ochre hue, L=95% to L=42% |
--gp-div-lo-2 through --gp-div-hi-2 | Blue to red through neutral midpoint |
Animation
| Property | Value |
|---|---|
--gp-ease | cubic-bezier(0.4, 0, 0.2, 1) |
--gp-ease-out | cubic-bezier(0, 0, 0.2, 1) |
Graphics Press CSS v4.5.0 · MIT License · ET Book typeface: Edward Tufte, Dmitry Krasny, Bonnie Scranton · Color: Eduard Imhof, Cartographic Relief Presentation