libelli
libelli is a free, open-source web app that turns the rows of a spreadsheet into print-ready cards. It runs entirely in the browser: there is no server and no account, nothing is uploaded, and it keeps working offline.
- Project page — https://heracl.es/libelli
- Live instance — https://libelli.vercel.app
- Source — https://github.com/Arty2/libelli
Name
Libelli is Latin, the plural of libellus: “little book”, a diminutive of liber, “book”. The dragonfly in the logo is a nod to Libellula, a genus of dragonflies whose name is thought to come from the same word.
Overview
A user pastes or imports a table, designs one card, and links each area on the card to a column. libelli then produces one card per row. Typical uses are labels, recipe cards, exhibition captions, revision cards, programmes and small booklets.
The card is laid out on a page measured in millimetres. An area can take its words from a column, hold fixed text, or show an image, a drawing or a barcode. Areas can follow one another down the page, grow or shrink to fit their content, and disappear when their column is empty, so a card without a subtitle has no gap where the subtitle would be. Layout can also read the columns and propose a first draft of the card. The editor and the printed page share one layout, so what is on screen is what is printed.
Every control in the app has a tooltip: hover over it, or press and hold it on a touchscreen. This page covers what a tooltip cannot.
Data
A table can be pasted straight from Excel, Google Sheets, Coda or any other spreadsheet, or imported as a CSV or TSV file. Quoted fields and line breaks inside a cell are kept. When pasting into a table that already has columns, no header row is needed: the cells fill the existing columns from left to right.
Cell content is always treated as text. A cell containing <b>x</b> prints
those characters, not bold type.
Column names contain no spaces: spaces become hyphens, and other punctuation is removed. This keeps every name usable as a placeholder (below).
Several tables and templates can be kept side by side, and they are independent of each other: one design can print any table, and one table can be printed with any design. A table exports as CSV; a template as JSON.
Placeholders
Text between double percent signs is replaced on each card. Placeholders work in an area’s own text and inside cells, and Markdown around them still applies.
| Placeholder | Prints |
|---|---|
%%title%% | The title column of this card’s row. Case is ignored. |
%%title:find:replace%% | The column, with every find replaced. The replacement may be empty; a literal colon is written \:. |
%%today%% | Today’s date. %%today:YYYY-MM-DD%% sets the format, with YYYY, YY, MM, DD, MMMM, MMM, dddd and ddd. |
%%lookup:3:price%% | The price column of row 3. Row numbers follow their rows when the table is sorted or rearranged. |
%%lookup:next:title%% | The same column in the next row; previous for the one before. |
%%page:current%%, %%page:total%% | The card’s page number, and how many pages there are. |
%%toc:title%% | A table of contents: each other page’s title with its page number. |
%%% | A tab: what follows is pushed to the right edge, as in Espresso %%% 2.20. The area’s Leader fills the gap with dots, dashes or a line. |
A placeholder that matches nothing is underlined in wavy red in the editor.
Typing %% in any text field offers the column names. A single % is ordinary
text. today, lookup, page and toc are reserved words; a column with one
of those names has to be renamed, or reached with lookup.
Markdown
Areas set to Markdown support a small subset: #, ## and ### headings,
bulleted lists (one level of nesting), numbered lists, **bold**, *italic*,
~~strikethrough~~, ==highlight==, `code`, [links](https://…),
paragraphs separated by a blank line, and --- for a rule. Anything else is
shown as written.
[some words]{red} colours just those words. Hex values, rgb(), hsl() and
the standard CSS colour names are accepted.
Template CSS
Each template can carry its own stylesheet. Its rules apply only inside the
card, and @import and external url() references are removed.
- An area is reachable by its name: an area called Job Title is
#Job-Title..areamatches every area. - Areas also carry classes for their content (
.content-field,.content-static,.content-image) and mode (.mode-plain,.mode-markdown,.mode-image,.mode-color,.mode-qr), and.columnswhen set in columns. - Pages are reachable as
#page-3,#cover,#back-cover,#inside-coverand#inside-back-cover, and as.rectoand.versowhen facing pages are on:#cover .area { … }. - The page settings are available as variables:
--page-w,--page-h,--margin-top(and the other three edges),--bleed,--text-font,--text-size,--text-leadingand--text-color.
Starter, in the CSS editor, writes out a sheet describing the current template — its page, fonts and every named area — which can be edited directly or handed to an AI model to style.
Printing
Export opens a preview of every card before anything is printed; untick a card to leave it out. From there the run can be printed, or saved as 300 dpi PNG files, bundled into one ZIP when there are several.
For the print to match the screen, set these in the browser’s print dialog:
- Paper size — the size of the card, or of the sheet when several cards share one.
- Margins — None.
- Headers and footers — off.
- Background graphics — on, or the paper colour and background images are dropped.
Print Settings can place 2, 4, 6 or 8 cards on one sheet, with bleed and crop marks. Its Order setting can also arrange the pages for folding:
- 2-up (folio) — a stapled booklet. Print double-sided, flipping on the long edge (if the backs come out upside down, use the other flip setting), fold the stack in half and staple the spine.
- 8-up (octavo) — the one-sheet mini zine. Print one side, fold in half three times, cut the centre of the middle fold, and fold into a booklet.
A booklet is padded with blank pages to a multiple of four (eight for the mini zine). With Recto / Verso on, left-hand pages mirror the layout of the right-hand ones across the fold.
Keyboard shortcuts
| Keys | Action |
|---|---|
| Ctrl/⌘ z | Undo |
| Ctrl/⌘ y | Redo |
| Ctrl/⌘ ⇧ z | Toggle the last change off and on, to compare |
| Enter | Type into the selected area |
| Esc | Stop typing, leave Select Multiple, deselect, or close what is open |
| Arrows | Nudge the selection 1 mm; with ⇧ 5 mm, with Alt ⇧ 10 mm |
| Arrows, PageUp / PageDown | Move between cards, with nothing selected |
| ⇧ / Ctrl / ⌘ + click | Add an area to the selection, or remove it |
| Ctrl/⌘ a | Select every area |
| Ctrl/⌘ d | Duplicate the selected areas |
| Ctrl/⌘ l | Lock or unlock the selected areas |
| Ctrl/⌘ ⇧ l | Lock or unlock the design |
| Delete / Backspace | Delete the selected areas |
| Ctrl/⌘ c / v | Copy an area’s words; paste text as a new area |
| Ctrl/⌘ ⇧ c / v | Copy an area’s style; paste it onto the selection |
| Ctrl/⌘ ⇧ + arrows | Change the alignment |
| Ctrl/⌘ ⇧ + scroll | Change the type size of the area under the pointer |
| Ctrl/⌘ + scroll, or pinch | Zoom the page; off the page, the interface’s text size |
| Ctrl/⌘ + / − / 0 | Zoom in, out, or fit (⇧ 0 for 100%) |
| Ctrl/⌘ h | Show or hide area outlines |
| Ctrl/⌘ ; or | | Cycle the guides |
| Ctrl/⌘ ’ or # | Show or hide the grid |
| Ctrl/⌘ s | Save the drawing, while drawing |
| Ctrl/⌘ p | Export; press again to print |
| Ctrl/⌘ ⇧ p or s | Export |
| ? or / | Help |
Holding Undo or Redo beside the page, or right-clicking either, lists every step it could take, so several can be skipped at once.
While typing in a field, undo and Delete act on the text, as usual. On a touchscreen, a pinch on the page zooms it; zoom and pan (under + Area) lets one finger scroll the page and adds an on-screen pad for nudging; and a long press on an area opens its menu.
Storage and offline use
Everything is saved automatically in the browser. Images and fonts are stored by name, so a template stays small and can be shared as a JSON file; whoever opens it is asked for any image or font they do not have. Drawings made in the app are the exception: they are saved inside the table cell, so they travel with the data. In Chromium-based browsers, images can be kept in a folder on disk instead.
Undo history is kept in memory, so it lasts until the page is reloaded; an update waits for the user to accept it for the same reason.
The app keeps a copy of itself and works offline after the first visit. Where the browser supports it, it can be installed as an app. Only one tab edits at a time; a second tab offers to take over.
What leaves this machine
Nothing that is typed, drawn or imported is ever uploaded. There is no backend and no analytics. Three kinds of request do reach the network, each because something was asked for by name:
- Google Fonts. Choosing a Google font, or opening a template that uses
one, loads it from
fonts.googleapis.com. A template from someone else can therefore trigger this request when opened. Only a valid font name is accepted. Uploading a font file avoids the request entirely. - A linked background image, when a template names one. Only
httpandhttpsaddresses are allowed. - The app’s own files, cached so it can work offline.
A template’s CSS cannot add to this list, and neither can a cell: a stored
image is written local:name, which a browser never treats as an address.
Development
libelli is a static SvelteKit site with no runtime dependencies; the Markdown renderer, the CSV parser and the barcode encoders are written by hand.
npm install
npm run dev # http://localhost:5173
npm run verify # project rules, lint, unit tests and type checks
npm run build # static output in ./build, for any static host
Versions are numbered 0.minor.patch, and the leading zero never changes:
| Change | Bumps | Example |
|---|---|---|
| A fix | the patch | 0.29.6 → 0.29.7 |
| A feature | the minor | 0.29.6 → 0.30.0 |
AGENTS.md explains the codebase and its rules,
docs/decisions.md the reasoning behind each module, and
CHANGELOG.md what each version added.
Credit
Dialectic Acheiropoieton of Heracles Papatheodorou and Claude
Released under the MIT License.