It looks like you're offline.
Open Library logo

Icons

Which technique

Two ways to render a glyph, both drawing from the same source (static/icons/src/), so an icon looks identical either way. Pick by who renders the markup.

Where the icon lives What to use
A server-rendered template, Templetor or Jinja. The default. $:macros.icon() in Templetor, {{ icon() }} in Jinja. One macro behind both — a plain inline <svg> against the sprite asset, one file cached sitewide. No JavaScript, no layout shift.
Anywhere the client renders the markup — a Lit template, a web component's own shadow DOM, or plain HTML handed to an outside consumer. The <ol-icon> element. One element everywhere: it inlines the glyph into its own shadow root, so it needs neither the sprite nor the global stylesheet and works just as well nested inside another component. The box is reserved so nothing shifts, but the glyph appears only once the JS upgrades it — prefer the macro whenever the markup is server-rendered.

Usage

In a Templetor template

Pass label for an accessible name; without one the icon is aria-hidden.

$:macros.icon("search") 
$:macros.icon("globe", size="lg")
$:macros.icon("globe", label=_("Language"))

In a Jinja template

The same macro, reached through the icon global — nothing to import or pass in.

{{ icon("search") }}
{{ icon("globe", size="lg") }}
{{ icon("globe", label=_("Language")) }}

In a Lit component

Import the element once, then use it like any other tag — including inside your own shadow root. Size it from your component's CSS; stroke width follows the size unless you set --ol-icon-stroke-width. The import is only for modules inside the Lit bundle; code in the page-JS bundle already has <ol-icon> registered site-wide and must not import it, or the second customElements.define() throws and takes the rest of the Lit bundle down with it.

import './OlIcon.js';

html`<ol-icon name="search"></ol-icon>`

As a custom element

Hand-written markup, or markup a script builds at runtime. Nothing to import — the main bundle registers <ol-icon> site-wide. Prefer the macro when the markup is server-rendered: the box is reserved up front, but the glyph only appears once the JS upgrades the element.

The same element, in plain markup.

<ol-icon name="search">
</ol-icon>
<ol-icon name="globe" size="lg" label="Language">
</ol-icon>
API reference
Properties
PropertyAttributeTypeDefaultDescription
name name String '' Icon name, e.g. "search". See /developers/design#icons.
size size 'sm' | 'md' | 'lg' 'md' "sm" (16px) | "md" (20px, default) | "lg" (24px).
label label String '' Accessible name; exposes the icon as role="img". Without it the icon is aria-hidden.
filled filled Boolean false Paint the glyph's interior in currentColor, for on/off states like a saved bookmark or a rated star.
CSS custom properties
PropertyDefaultDescription
--ol-icon-stroke-width — Stroke weight, overriding the size default.

What you control

Size

Preset Box Stroke, as drawn Reach for it when
sm 16px 1rem 1.33px Inline with body text, dense toolbars.
md 20px 1.25rem 1.67px The default. Almost everything — buttons, menu rows, form affordances.
lg 24px 1.5rem 1.75px Standalone icon buttons, section headers.

Size scales the stroke with it. The box scales the whole 24×24 grid, geometry and stroke together, so the weight stays a roughly constant fraction of the icon rather than a fixed hairline. That is deliberate: a fixed stroke would make sm read as a heavier, different family. lg is the one step pulled back from the raw ratio — see stroke weight.

Each preset is a token — --icon-size-sm, -md, -lg — declared in rem, so the pixel figures above hold at the default 16px root and grow with the reader's font size. The macro and <ol-icon> both take size="sm|md|lg" — there is no off-scale value. A genuine one-off is a CSS job: set width/height on the <ol-icon> host (the inner <svg> fills it) or on a fragment import's own wrapping <svg>.

$:macros.icon("search", size="sm")
$:macros.icon("search", size="md")
$:macros.icon("search", size="lg")

Color

Every glyph is drawn in currentColor, so an icon is simply the text color of wherever it sits. Recolor it by setting color on the icon or any ancestor — say .danger { color: var(--color-error-fg) } — with no fill or stroke override needed.

<span class="danger">$:macros.icon("trash")</span>

Stroke weight

One weight across the whole set: 2 units on the 24×24 grid, which the box scales down with the geometry — 1.33px at sm, 1.67px at md. Carried through, lg would land at a full 2px and read noticeably heavier than the same glyph one step down, so it is corrected to 1.75. Absolute weight then stays near-constant across the scale (1.33 / 1.67 / 1.75px) instead of growing with the box.

Weight is a per-step token — --icon-stroke-sm, -md, -lg — and not baked into the sprite: the build strips stroke-width from each <symbol> precisely so the value can be inherited from the referencing <svg>. (A presentation attribute on the symbol would outrank any inherited value, and CSS cannot select into a <use> shadow tree, so a baked weight would be unoverridable.) Tune the set from those tokens; a component with a genuine reason can set stroke-width on its own wrapper, but that is the exception.

Where glyphs come from

Every glyph is a plain SVG file committed under static/icons/src/<source>/, normalized to the canonical form — 24×24 grid, 2px stroke, currentColor. The folder name records provenance and nothing else: the build reads every folder identically, and callers address bare names (icon("search")) that never mention a library.

Why Lucide

Most geometry today is vendored from Lucide, the ISC-licensed community fork of Feather — chosen because its design language already is our canonical form, and its coverage makes most icon needs a copy-and-normalize away. That is a head start, not an attachment: there is no package dependency and no runtime coupling, so replacing a glyph's geometry later changes one file, never an API or a call site.

Mixing sources is the design

custom/ holds original Open Library glyphs, and glyphs adapted from another library get a folder of their own — the build discovers new folders automatically. The one real rule is aesthetic, not technical: a newcomer must be redrawn to the canonical form so the set still reads as a single family. The recipe is below.

Adding an icon

  1. Author the SVG in the canonical form — viewBox="0 0 24 24", stroke="currentColor", no width/height, no ids. A glyph copied from lucide.dev is already in this form — paste it verbatim. (A stroke-width in the source is fine; the build strips it so weight stays themable.)
  2. Name the file in kebab-case and drop it in the folder matching its provenance — static/icons/src/lucide/ for Lucide geometry, src/custom/ for original Open Library work. The file name becomes the icon's name everywhere: pencil.svg is icon("pencil"). A glyph from a new library gets a new folder plus a license section in static/icons/ATTRIBUTION.md.
  3. Run make icons. One step rebuilds both outputs: static/icons/sprite.svg and the Lit glyph module the <ol-icon> bundle imports. Neither is committed, so the source SVG is the only file in the commit.

That's it — the icon now renders through both techniques and appears in the gallery above.