-
-
Notifications
You must be signed in to change notification settings - Fork 1.8k
frontend guide
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.
Common commands for frontend development. All commands run inside Docker.
| Task | Command |
|---|---|
| Build everything | docker compose run --rm home npm run build-assets |
| Build JS | docker compose run --rm home make js |
| Build CSS | docker compose run --rm home make css |
| Watch JS | docker compose run --rm home npm run-script watch |
| Watch CSS | docker compose run --rm home npm run-script watch:css |
| Watch components | docker compose run --rm home npm run-script watch:components |
| Lint JS/CSS | docker compose run --rm home npm run lint |
| Lint Python | docker compose run --rm home make lint |
| Clear home page cache | docker 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.
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.
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).
/* 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.
/* ❌ 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.
/* ❌ 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.
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>.
When adding CSS, you may encounter:
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.
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).
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.
// 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:
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.
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 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:
-
Template — the page where the partial renders (
openlibrary/templates/) -
Partial template — a macro in
openlibrary/macros/ -
Partial JS — fetches data and inserts the rendered partial (
openlibrary/plugins/openlibrary/js/) - index.js — connects the DOM element to the partial's JS
-
partials.py — the endpoint that renders the macro with data (
openlibrary/plugins/openlibrary/partials.py)
Connection pattern:
- In the template, create a placeholder element with an id, or call the partial macro directly
- In
index.js, select the placeholder and import the partial's JS when it exists - The partial's JS calls the partials endpoint, which returns the data-infused macro HTML
We support Firefox and Chromium-based browsers on desktop and mobile (iOS and Android).