Skip to content

Frontend Reference

Detailed reference for working with CSS, JavaScript, routing, and partials in the Open Library codebase. For an introduction to the frontend architecture and how to get started, see the Frontend overview and Making Your First Frontend Change.

Quick Reference

Common commands for frontend development. All commands run inside Docker.

TaskCommand
Build everythingdocker compose run --rm home npm run build-assets
Build JSdocker compose run --rm home make js
Build CSSdocker compose run --rm home make css
Watch JSdocker compose run --rm home npm run-script watch
Watch CSSdocker compose run --rm home npm run-script watch:css
Watch componentsdocker compose run --rm home npm run-script watch:components
Lint JS/CSSdocker compose run --rm home npm run lint
Lint Pythondocker compose run --rm home make lint
Clear home page cachedocker compose restart memcached

TIP

Template (HTML) changes auto-reload — no build step needed. JS and CSS changes require a rebuild or a running watch process.

Working with CSS

CSS Build Process

Stylesheets are compiled via webpack to generate individual CSS files in static/build/css/ (e.g., page-home.css, page-book.css). Each page loads the appropriate page-specific CSS file.

Check out the CSS directory README for information on render-blocking vs JS-loaded CSS files.

CSS Conventions

Use BEM notation. We use BEM for CSS class naming in templates and global styles. The exceptions are Web Components and Vue components, which have built-in CSS encapsulation (Shadow DOM and <style scoped> respectively).

css
/* Block */
.book-card {
}

/* Element (part of the block) */
.book-card__title {
}
.book-card__cover {
}

/* Modifier (variation of block or element) */
.book-card--featured {
}
.book-card__title--large {
}

Avoid styling bare HTML elements.

css
/* ❌ Affects every paragraph globally */
p {
  margin-bottom: 1rem;
}

/* ✅ Explicit class */
.book-description__text {
  margin-bottom: 1em;
}

Avoid IDs for styling. IDs have high specificity and are meant for JavaScript hooks or anchor links, not styling.

Avoid deep nesting. Flat selectors are easier to find and override.

css
/* ❌ Deeply nested */
.book-list .book-card .book-card__title {
}

/* ✅ Flat */
.book-card__title {
}

Use design tokens, not magic numbers. Always use semantic tokens (e.g. var(--border-radius-card)) instead of primitives or hardcoded values. See the Design Token Guide for the two-tier architecture and usage examples.

Applying CSS to Templates

Certain templates define a cssfile variable using putctx()see examples on GitHub. The body sub-template sets this variable via putctx(), which passes it up to the wrapping site.html template, which loads the corresponding CSS file in the <head>.

Bundle Sizes

When adding CSS, you may encounter:

text
FAIL static/build/page-plain.css: 18.81KB > maxSize 18.8KB (gzip)

This means your changes exceeded the CSS payload limit. This is especially important for CSS on the critical path. Consider placing styles in a JavaScript entrypoint file (e.g., <file_name>--js.css) and loading it via JavaScript, which has a higher bundlesize threshold.

Working with JavaScript

JavaScript files live in openlibrary/plugins/openlibrary/js. Custom files are combined into build/js/all.js. Third-party libraries go in vendor/js and are combined into build/vendor.js (specified in static/js/vendor.jsh).

Linking JavaScript to Templates

This tutorial walks through connecting a new JS file to an HTML template, using a team page filter as an example.

Step 1: Create a JS file in openlibrary/plugins/openlibrary/js/ with a meaningful name (e.g., team.js).

Step 2: In index.js, add a DOM-based loader. The pattern: query for an element that only exists on your target page, then dynamically import your JS when it's found.

js
// Add functionality to the team page for filtering members:
const teamCards = document.querySelector(".teamCards_container");

if (teamCards) {
  import("./team").then((module) => {
    if (teamCards) {
      module.initTeamFilter();
    }
  });
}

Step 3: Export an init function from your JS file:

js
export function initTeamFilter() {
  console.log("Hooked up");
}

Build with docker compose run --rm home make js or use the watch script, then reload the page. You should see "Hooked up" in the console.

TIP

For interactive UI that needs encapsulation and reusability, consider a Lit web component instead of this pattern.

URL Routing

Most routing is in openlibrary/plugins. Some routes (like /books/OL..M/:title) pass through to Infogami — see route patterns at the bottom of openlibrary/core/models.py.

See also: The Lifecycle of a Network Request | Adding a new Router

Partials

Partials let a page load quickly with minimal HTML, then fetch additional components asynchronously via JavaScript. For example, book prices in the sidebar load after the main page renders.

A Partial is a targeted endpoint returning minimal HTML for a specific widget (e.g., "related books carousel" or "book price widget"). See PR #8824 for a complete example.

Files involved:

  1. Template — the page where the partial renders (openlibrary/templates/)
  2. Partial template — a macro in openlibrary/macros/
  3. Partial JS — fetches data and inserts the rendered partial (openlibrary/plugins/openlibrary/js/)
  4. index.js — connects the DOM element to the partial's JS
  5. partials.py — the endpoint that renders the macro with data (openlibrary/plugins/openlibrary/partials.py)

Connection pattern:

  1. In the template, create a placeholder element with an id, or call the partial macro directly
  2. In index.js, select the placeholder and import the partial's JS when it exists
  3. The partial's JS calls the partials endpoint, which returns the data-infused macro HTML

Browser Support

We support Firefox and Chromium-based browsers on desktop and mobile (iOS and Android).