CSSV v1 · Examples

The CSV you already have, worth looking at.

Every table on this page is one .cssv file: a CSS style block between two --- lines, then ordinary CSV, rendered live by <cssv-table>. The first ten were made for this page, to show how far a style block goes. The next eight start from files people already have. The last two show other ways to put a table in a page. Files used in ways CSSV wasn't made for, from a film to a lava lamp, are on the experiments page.

Each tile below is the file itself, rendered live and scaled down. Point at one to see its plain CSV; click it for the full card. In a card, the yellow figure is the size of the style block, and View source shows the whole file.

Made for the gallery

Real usage examples

Made for the gallery

These files were written for this page, to show how far a style block goes. Each is still a plain table, one record per row; the style block turns it into a grid of tiles, a resume, a stadium, a cabin, a week stood on its side, a food label or a trading terminal.

Display numbers in
Files store 1234.5; the reader's locale decides how it looks.

The periodic table

elements.cssv

118 records in atomic-number order. The style block makes tbody an 18-column grid, so tr:nth-child(26) is iron: most elements flow into place, and five rules move the rest. --cssv-key: category names every row by its category, and each category sets one color. Masses keep the digits written in the file; switch to de-DE for decimal commas.

  • data: the 118 elements
  • tbody { display: grid }
  • :nth-child(n+57):nth-child(-n+71)
  • --cssv-key
  • tbody::after
  • @container
Alkali metal Alkaline earth metal Transition metal Post-transition metal Metalloid Nonmetal Halogen Noble gas Lanthanide Actinide Unknown

A resume

resume.cssv

One row per line of the resume: a job, one of its points, a skill. A kind column says which, and as the key column it is what every rule selects on. Rows are subgrids of one grid, so dates, places and labels share a left column, and the rail beside it carries a timeline drawn by each row's ::before. [data-key="heading"] + [data-key="job"] finds the current job and fills its dot. The headings are rows too, so they stay in the data.

  • data: an invented person
  • --cssv-key: kind
  • grid-template-columns: subgrid
  • [data-key="heading"] + [data-key="job"]
  • @container
  • @media print
On paper, the file's @media print rules set it on two A4 pages.

Stadium tickets

stadium.cssv

A seating plan exported from Ticketlark, an invented ticket seller, for Northbridge FC v Harbor City: one record per section, with its stand, tier, category, price, seats and the seats left. The records run clockwise from the halfway line, tier by tier, so sibling-index() turns each row's place in the file into an angle, and the row's ::before cuts its wedge out of the bowl with clip-path: polygon(). The category is the key column, so one rule colors every section in a category, and a section with no seats left is .zero, hatched. Point at a section and tr:hover lights it up and shows the rest of its record over the pitch. Browsers older than Chrome 138, Safari 26.2 and Firefox 154 have no sibling-index() and show the sections as a list.

  • data: an invented match
  • sibling-index()
  • clip-path: polygon()
  • [data-key="Category 1"]
  • tr:has(.zero[data-col="available"])
  • tr:hover
  • table.update()
Category 1 Category 2 Category 3 Category 4 Hospitality Sold out
Every half second the page hands the table the next export with update(), which changes only the cells that changed.

Seat map

seats.cssv

A booking export: initials where a seat is taken, an empty field where it's free, so :empty draws the free seats. The row number is the middle column, which puts it in the aisle, and the key column, so tr[data-key="12"] is an exit row and one rule finds your seat by row and column. There is no row 13.

  • data: invented bookings
  • :empty
  • tr[data-key="17"] td[data-col="D"]
  • :nth-child(-n+3)
  • @keyframes
Free Taken Your seat Exit

A week of weather

forecast.cssv

Each record is a day, and the style block stands the table on its side: every tr becomes a column of the table's grid, sharing its rows through subgrid, and the header row becomes the labels. The condition is the key column, and each condition paints its own sky and icon with gradients. Friday's low of −0.3 rounds to 0 but keeps .negative, so it stays frost blue. Try ar-EG.

  • data: an invented week
  • grid-template-rows: subgrid
  • rows as columns
  • tr[data-key="storm"]
  • .negative
  • colgroup { display: none }

Nutrition Facts

nutrition.cssv

Four columns: nutrient, amount, unit and % Daily Value. Every row is a flex line, and the nutrient, the key column, styles it by name: the heavy rules, the indents, the order that makes "Includes 10g Added Sugars", and the title and footnote as generated content. Empty % cells disappear with :empty.

  • data: an invented label
  • tr { display: flex }
  • order
  • ::before / ::after
  • :empty

Market close

markets.cssv

A trading terminal built from sign classes. Every number cell is tagged .positive, .negative or .zero, which drives the arrows, the colors, the pill tints and the row highlights. No value is repeated in the CSS.

  • data: invented tickers
  • .positive / .negative / .zero
  • tr:has(.negative)
  • --cssv-format per column
  • tr[data-key="QBIT"]
  • @keyframes

The solar system

planets.cssv

--cssv-key: planet names every row, and each row sets a few custom properties (--orb, --size, --glow) that one shared rule draws. The file stores 0.387, and --cssv-format rounds it to 0.39 for display.

  • data: the eight planets
  • --cssv-key
  • custom properties per row
  • ::before / ::after
  • rounding: halfExpand
  • grouping by locale

Invoice

invoice.cssv

SKUs like 0042 stay text because of the leading zero. Quoted fields span two lines, and ::first-line bolds the title. Summary rows have no SKU, so they get no data-key and tr:not([data-key]) selects them. Switch to de-DE or fr-FR to move the € sign after the amount with :lang().

  • data: an invented invoice
  • text IDs
  • multi-line fields
  • tr:not([data-key])
  • :lang(de)
  • @media print

League table

league.cssv

A semicolon-delimited spreadsheet export, detected from the header. Ranks come from a CSS counter, and crests come from two colors set per club by name. Zones are selected by position. The style block switches palettes with prefers-color-scheme.

  • data: an invented league
  • ; delimiter
  • counter(rank)
  • :nth-child zones
  • prefers-color-scheme
Title race Cup qualifier Relegation

Real usage examples

Each card starts from a file people already have: a download, an export, a command's output. The data stays exactly as it was exported; the two that print text rather than CSV, WhatsApp and git log, go through a few lines of Node first. As exported shows what you started with.

A weather download

oslo.cssv · berlin.cssv · lisbon.cssv · weather.css

Who has it: anyone who pulls data from an open-data portal or API. These are real: January 2026, daily, from Open-Meteo's archive, saved as CSV and left untouched, preamble and all.

What the style does: each file is one @import; the calendar lives in weather.css, written once. The sign of each value says what kind of day it was: a negative minimum is a frosty night, a negative maximum a day below zero, a positive snowfall a snow day. The download's two location lines are found by data-row and become the caption.

  • data: the download, unchanged
Weather data by Open-Meteo.com (CC BY 4.0).

A Jira export, as a board

jira-export.cssv

Who has it: anyone who has clicked "Export CSV" in Jira to share a sprint with someone who has no account. The export repeats a column for every sprint and every label, all under the same name.

What the style does: --cssv-key: Status sends each issue to its lane with one rule per status, and dense packing stacks the cards. Status is text, which v1 can't style by value; as the key it can. The repeated columns are no problem: [data-col="Labels"] styles both label columns.

  • sample rows in Jira's format

A WhatsApp group chat

whatsapp-chat.cssv

Who has it: anyone in a WhatsApp chat, through "Export chat" in the chat's menu. The export is a text file with a line per message; a few lines of Node turn each message into a CSV line: date, time, sender, message.

What the style does: --cssv-key: sender puts the messages of Sam, who exported the chat, on the right and gives everyone else a color. WhatsApp's own lines, like "Ana added you", have no sender, so they get no data-key, and tr:not([data-key]) centers them. A message with line breaks is one quoted field, and the renderer keeps the breaks.

  • sample chat; the people are invented

A Google Calendar import

calendar-import.cssv

Who has it: anyone who has put a term schedule, a shift rota or a fixture list into a spreadsheet to bulk-import it into Google Calendar, which reads CSV with exactly these column names.

What the style does: --cssv-key: "Start Date" and five rules put each date in its column of a week, and a day's events stack in time order. The format leaves the times empty for all-day events, so :empty turns those into solid banners. The same file still imports.

  • sample rows in Google Calendar's format

A Canvas gradebook

grades-bio-101.cssv

Who has it: every teacher on Canvas. Line 2 isn't a student: it's the "Points Possible" row, indented with four spaces.

What the style does: tr[data-row="2"] styles that row as "out of", without moving it. An empty score (:empty) is a missing assignment and says so; a 0 (.zero) was handed in and scored zero. Students with something missing get a red dot. The ID columns are hidden.

  • sample rows in Canvas's format

A translation sheet

strings.cssv

Who has it: any team that ships an app in more than one language. Translation tools import and export this CSV, and it often lives in the repository.

What the style does: every empty cell is an untranslated string, so td:empty marks it in place, and a CSS counter per language totals them under the table. Reviewers see the gaps in the file itself, not in a separate report.

  • sample strings

git log, as a changelog

git-log.cssv

Who has it: anyone with a terminal. Any command that prints CSV can print a report. These are the last 18 commits of this repository, from git log --shortstat.

What the style does: deleted lines are written as negative numbers, so .positive and .negative color the additions and deletions, and .zero fades the commits that only added.

  • data: this repository's history

An attendee list, printed as name badges

attendees.cssv

Who has it: anyone running a meetup or a conference, the night before. Usually this is a mail merge in a word processor.

What the style does: each row becomes a 9 × 5.5 cm badge, and @media print lays them out two to a row for an A4 page. The ticket type is the key, so tr[data-key="Speaker"] gets its own color. The order number, email and check-in columns are hidden; they stay in the file for the door.

  • sample rows; the people are invented
On paper, the file's @media print rules make a sheet two badges wide, ready to cut.

On a page

The cards above load their files with src. These two show the other ways in: a file written inline in the page, and a table that pages through a server's results.

Release notes

inline <script type="text/cssv">

This file sits inline in the page instead of being loaded with src. Prefix selectors on the key ([data-key^="feat-"]) color each entry, and the timeline is drawn with pseudo-elements. Colors are mixed from currentColor, so the table follows the page's light or dark theme.

  • data: invented releases
  • inline <script>
  • [data-key^="break-"]
  • color-mix(currentColor)
  • ::first-line

Account activity, a page at a time

ledger/page-1.cssv

A server can send .cssv where it would send JSON. This site is static, so the five pages are files; a real endpoint would take ?page=2 and report the total, for example in a response header. The page script sets the table's src and listens for its loading events.

Each page's style block is a comment and @import url("ledger.css"). The shared stylesheet sets the key column and the number format and styles the rows, so a page carries only its records and the browser can cache the styles. The current page stays on screen while the next one loads, and cssv-loadstart and cssv-loadend drive the spinner; throttle the network in your browser's developer tools to see it. Click ahead quickly and only the last page you asked for is shown.

  • data: invented transactions
  • table.src = …
  • cssv-loadstart / cssv-loadend
  • shared @import
  • .positive::before

Why a style block, and not a script or a spreadsheet

What these files ran into