Styleguide
Every house component, rendered from the templates and the stylesheet this
instance ships. Copy the source under a component. When you add a
component, add its entry here (base/templates/styleguide/entries/);
manage.py check_catalogue fails without one.
Tokens
The values as this page computes them, so an instance sees its own. Style a component with a token, never with a literal colour, radius or shadow; an instance restyles the platform by overriding tokens.
| Token | Sample | Value here | Role |
|---|---|---|---|
--bs-primary
|
|
primary action, links, brand | |
--bs-primary-rgb
|
|
the same colour, for Bootstrap's utilities | |
--bs-secondary
|
|
secondary (outline) actions | |
--bs-secondary-rgb
|
|
the same colour, for Bootstrap's utilities | |
--bs-body-color
|
|
text | |
--bs-body-color-rgb
|
|
the same colour, for Bootstrap's utilities | |
--oep-text-muted
|
|
secondary text | |
--oep-heading-color
|
|
platform headings | |
--bs-border-color
|
|
borders, dividers | |
--oep-border-strong
|
|
field borders | |
--oep-surface
|
|
cards, overlays | |
--oep-surface-subtle
|
|
light bands, the header band, table heads | |
--oep-surface-strong
|
|
blue bands (hero, academy, footer) | |
--oep-surface-hover
|
|
hover tint (the navbar dropdown's) | |
--oep-on-strong
|
|
text on blue bands | |
--oep-on-strong-muted
|
|
muted text on blue bands | |
--bs-success
|
|
status: success, never an action | |
--bs-success-rgb
|
|
the same colour, for Bootstrap's utilities | |
--bs-info
|
|
status: information, never an action | |
--bs-info-rgb
|
|
the same colour, for Bootstrap's utilities | |
--bs-warning
|
|
status: warning, never an action | |
--bs-warning-rgb
|
|
the same colour, for Bootstrap's utilities | |
--bs-danger
|
|
status: error; destructive actions | |
--bs-danger-rgb
|
|
the same colour, for Bootstrap's utilities | |
--bs-border-radius
|
|
in the page: buttons, fields, cards | |
--bs-border-radius-sm
|
|
small buttons, badges | |
--bs-border-radius-lg
|
|
what floats: dropdowns, dialogs | |
--bs-border-radius-pill
|
|
tags | |
--oep-shadow-sm
|
|
cards | |
--oep-shadow-lg
|
|
overlays: dropdowns, dialogs, toasts | |
--oep-shadow-rgb
|
|
the tint of both shadows | |
--bs-font-sans-serif
|
|
the font (a system stack, no web font) | |
--bs-body-font-size
|
|
text | |
--oep-font-size-dense
|
|
tables, badges, sidebars, buttons, fields | |
--oep-heading-weight
|
|
headings | |
--oep-measure
|
|
reading width for prose | |
--oep-content-width
|
|
content column and header band; 82, 108, 132 or 160rem by screen width | |
--oep-transition
|
|
hover motion |
alert
A status message on the page: the result of an action, a warning about the page's subject, an error. Light background, a 4px bar and an icon in the status colour. Write it with the include; a script's `alert alert-<status>` gets the same look.
<div class="d-grid gap-2">
{% include "components/alert.html" with status="success" text="Your profile was saved." %}
{% include "components/alert.html" with status="info" text="This table is a draft; only you can see it." %}
{% include "components/alert.html" with status="warning" text="The metadata does not name a license yet." %}
{% include "components/alert.html" with status="danger" text="The upload failed in line 12, column 3." dismissible=True %}
</div>
badge
A status beside its subject (Published, Draft, Early Access, what the viewer may do), or a neutral count; never a tag and never a pill.
Published Draft Early Access Embargoed 12
<p class="d-flex flex-wrap gap-2 align-items-center mb-0">
<span class="badge badge--success">Published</span>
<span class="badge badge--warning">Draft</span>
<span class="badge badge--info">Early Access</span>
<span class="badge badge--danger">Embargoed</span>
<span class="badge badge--neutral">12</span>
</p>
button
Every action, in one of four roles on the element itself: primary for the one main action of a view or dialog, secondary for the rest, danger outlined on the button that opens a confirmation and solid only inside it, link for an action that reads as a link; on a blue band the light button. Green, cyan, yellow and black are status colours, never a button.
<div class="d-flex flex-wrap gap-2 align-items-center mb-3">
<button type="button" class="btn btn-primary">Save changes</button>
<button type="button" class="btn btn-secondary">Cancel</button>
<button type="button" class="btn btn-outline-danger">Delete…</button>
<button type="button" class="btn btn-danger">Delete table</button>
<button type="button" class="btn btn-link">Show details</button>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center mb-3">
<button type="button" class="btn btn-primary btn-sm">Small primary</button>
<button type="button"
class="btn btn-secondary btn-sm">Small secondary</button>
<button type="button"
class="btn btn-outline-danger btn-sm">Small danger</button>
<button type="button" class="btn btn-link btn-sm">Small link</button>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center mb-3">
<button type="button"
class="btn btn-primary"
disabled>Disabled primary</button>
<button type="button"
class="btn btn-secondary"
disabled>Disabled secondary</button>
<a class="btn btn-secondary disabled"
aria-disabled="true">Disabled link button</a>
<button type="button" class="btn btn-primary">
<span class="btn__icon fas fa-plus" aria-hidden="true"></span>With an icon
</button>
</div>
<div class="d-flex flex-wrap gap-2 align-items-center p-3 rounded"
style="background-color: var(--oep-surface-strong)">
<a class="btn btn-on-strong btn-sm" href="#">Sign up</a>
<a class="btn btn-on-strong" href="#">On a blue band</a>
</div>
card
One item of a list or grid (a dataset, a topic, an organization) or one block of a page. Clickable when the whole card leads to one page: its title link is a stretched link, and nothing else in it is a link.
A plain card
Holds what belongs together: a title, a sentence or two, and the links of its own.
A link of its ownA clickable card
The whole card opens the page its title names.
<div class="row g-3">
<div class="col-12 col-md-6">
<div class="card h-100">
<div class="card-body">
<h3 class="card-title">A plain card</h3>
<p class="card-text">
Holds what belongs together: a title, a sentence or two, and the links of its own.
</p>
<a href="#card">A link of its own</a>
</div>
</div>
</div>
<div class="col-12 col-md-6">
<div class="card card--clickable h-100">
<div class="card-body">
<h3 class="card-title">
<a class="stretched-link"
href="{% url 'dataedit:topic-list' %}">A clickable card</a>
</h3>
<p class="card-text">The whole card opens the page its title names.</p>
</div>
</div>
</div>
</div>
confirm_dialog
Before an action the user may regret: a title naming the action, what will happen, Cancel and the verb (solid red when it cannot be undone; typed for a high bar). Never the browser's confirm(); a template asks with hx-confirm or data-confirm, a script with window.oep.confirm.
<div class="d-grid gap-4">
{% include "components/confirm_dialog.html" with as_example=True title="Publish table?" text="Everyone can then find and read the table." verb="Publish" %}
{% include "components/confirm_dialog.html" with as_example=True title="Remove member?" text="Ada Lovelace leaves Grid research and loses every permission the organization gave her." verb="Remove member" danger=True %}
{% include "components/confirm_dialog.html" with as_example=True title="Delete table?" text="The table, its data and its metadata are deleted. This cannot be undone." verb="Delete table" danger=True typed="wind_farms" %}
<div class="d-flex flex-wrap gap-2">
<button type="button"
class="btn btn-secondary"
data-confirm="Everyone can then find and read the table."
data-confirm-title="Publish table?"
data-confirm-verb="Publish">Try plain</button>
<button type="button"
class="btn btn-outline-danger"
data-confirm="Ada Lovelace leaves Grid research and loses every permission the organization gave her."
data-confirm-title="Remove member?"
data-confirm-verb="Remove member"
data-confirm-danger>Try danger</button>
<button type="button"
class="btn btn-outline-danger"
data-confirm="The table, its data and its metadata are deleted. This cannot be undone."
data-confirm-title="Delete table?"
data-confirm-verb="Delete table"
data-confirm-danger
data-confirm-typed="wind_farms">Try typed</button>
</div>
</div>
dropdown
A short list of choices or actions behind one button or tab; every dropdown looks like the navbar's, with no class of its own.
<div class="d-flex flex-wrap gap-4 align-items-start">
<div class="dropdown">
<button class="btn btn-link dropdown-toggle"
type="button"
data-bs-toggle="dropdown"
aria-expanded="false">Metadata options</button>
<ul class="dropdown-menu">
<li>
<h6 class="dropdown-header">Download</h6>
</li>
<li>
<a class="dropdown-item" href="#dropdown">JSON</a>
</li>
<li>
<a class="dropdown-item" href="#dropdown">Open the metadata API page</a>
</li>
<li>
<hr class="dropdown-divider" />
</li>
<li>
<a class="dropdown-item" href="#dropdown">Edit</a>
</li>
</ul>
</div>
<ul class="dropdown-menu d-block position-static"
aria-label="The same menu, shown open">
<li>
<h6 class="dropdown-header">Download</h6>
</li>
<li>
<a class="dropdown-item" href="#dropdown">JSON</a>
</li>
<li>
<a class="dropdown-item" href="#dropdown">Open the metadata API page</a>
</li>
<li>
<hr class="dropdown-divider" />
</li>
<li>
<a class="dropdown-item" href="#dropdown">Edit</a>
</li>
</ul>
</div>
empty_state
Where a list or a search has nothing to show: a muted icon, one sentence saying so, at most one action (what makes the list fill). Compact inside a card or a drawer. An error page is one at page size: an image (image, image_alt) instead of the icon, and with heading the title is the page's one h1.
No tables are assigned yet.
This page could not be found!
It might be an old link or maybe the data moved.
{% load static %}
{% include "components/empty_state.html" with icon="fas fa-layer-group" title="You have no datasets yet" text="A dataset collects tables under one title." action_template="styleguide/entries/_empty_state_action.html" %}
<div class="card">
<div class="card-body">
{% include "components/empty_state.html" with icon="fas fa-table" text="No tables are assigned yet." compact=True %}
</div>
</div>
{% static 'img/OpenEnergyFamily_OEPlatypus.png' as platypus %}
{% include "components/empty_state.html" with image=platypus image_alt="OEPlatypus" heading=True as_example=True title="This page could not be found!" text="It might be an old link or maybe the data moved." %}
field
Every form field: Bootstrap's form-control, form-select and form-check, label above, muted help and the error below, never a table. Render a Django form with django-bootstrap5 ({% bootstrap_field %}); allauth's forms get it from their element overrides. On these classes set longhands only: a padding shorthand on a select deletes the room for its arrow.
<div class="row g-4">
<div class="col-md-6">
<div class="mb-3">
<label class="form-label" for="field-name">Table name</label>
<input class="form-control"
type="text"
id="field-name"
placeholder="e.g. wind_farms_2024"
aria-describedby="field-name-help" />
<div class="form-text"
id="field-name-help">Lower case letters, digits and underscores.</div>
</div>
<div class="mb-3">
<label class="form-label" for="field-email">E-Mail</label>
<input class="form-control is-invalid"
type="email"
id="field-email"
value="not-an-address"
aria-describedby="field-email-error" />
<div class="invalid-feedback"
id="field-email-error">Enter a valid email address.</div>
</div>
<div class="mb-3">
<label class="form-label" for="field-description">Description</label>
<textarea class="form-control" id="field-description" rows="3"></textarea>
</div>
<div class="mb-3">
<label class="form-label" for="field-disabled">Current address</label>
<input class="form-control"
type="email"
id="field-disabled"
value="owner@example.org"
disabled />
</div>
</div>
<div class="col-md-6">
<div class="mb-3" style="max-width: 12rem">
<label class="form-label" for="field-narrow">The license under which the data of this table is published</label>
<select class="form-select" id="field-narrow">
<option>Creative Commons Attribution 4.0 International</option>
<option>Open Data Commons Open Database License</option>
</select>
</div>
<div class="mb-3">
<label class="form-label" for="field-small">Rows per page</label>
<select class="form-select form-select-sm" id="field-small">
<option>25</option>
<option>100</option>
</select>
</div>
<div class="form-check">
<input class="form-check-input"
type="checkbox"
id="field-check"
checked />
<label class="form-check-label"
for="field-check">Notify me about reviews</label>
</div>
<div class="form-check mb-3">
<input class="form-check-input"
type="radio"
name="field-radio"
id="field-radio" />
<label class="form-check-label" for="field-radio">Only this table</label>
</div>
</div>
</div>
field_help
The "?" beside a field's label when the label alone does not say what belongs in the field; its help opens on hover and on keyboard focus. Not for text every reader needs, which goes below the field as its help text.
Methodical focus
<p>
Methodical focus
{% include "components/field_help.html" with label="Methodical focus" text="The one to three areas the model concentrates on." %}
</p>
link_with_arrow
A "go to ..." link that stands on its own line or at the foot of a card, never inside running text; its arrow slides on hover.
<a class="link-with-arrow" href="{% url 'dataedit:topic-list' %}">
Explore the database
{% include "icons/arrow_right.html" %}
</a>
page_header
The band under the navbar on every page except the account and error pages: the area with its icon, then the trail back (at most two links) ending in the page's subject, its one h1. Optional: one muted line, the page's actions (at most one primary), a tab row on its lower edge (the dashboard's, with the gear aside link to Settings, #2653). On a page the title is its h1; here, `as_example` keeps the catalogue's one h1.
<div class="d-grid gap-3">
{% include "components/page_header.html" with area="About" icon="icons/area_about.html" crumb_label="Overview" crumb_url="#page_header" title="Open Energy Family Steering Committee" as_example=True %}
{% include "components/page_header.html" with area="Account" icon="icons/area_account.html" crumb_label="Dashboard" crumb_url="#page_header" crumb2_label="Settings" crumb2_url="#page_header" title="Update account profile" description="One muted line, when the page has one." actions="styleguide/entries/_page_header_actions.html" as_example=True %}
{% include "components/page_header.html" with area="Dashboard" icon="icons/area_dashboard.html" title="A user's name" tabs="styleguide/entries/_page_header_tabs.html" as_example=True %}
</div>
pagination
The pager under a paged list: separate buttons, the current page solid, an optional muted range at the end. DataTables draws the same pager.
<nav aria-label="Pages">
<ul class="pagination mb-0">
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">‹</span>
</li>
<li class="page-item active" aria-current="page">
<span class="page-link">1</span>
</li>
<li class="page-item">
<a class="page-link" href="#pagination" aria-label="Page 2">2</a>
</li>
<li class="page-item">
<a class="page-link" href="#pagination" aria-label="Page 3">3</a>
</li>
<li class="page-item disabled">
<span class="page-link" aria-hidden="true">…</span>
</li>
<li class="page-item">
<a class="page-link" href="#pagination" aria-label="Page 83">83</a>
</li>
<li class="page-item">
<a class="page-link" href="#pagination" aria-label="Next page">›</a>
</li>
<li class="pagination__range">1–25 of 2,068</li>
</ul>
</nav>
section_card
One section of a record page (a factsheet): its heading is a button that shows and hides the section, and every section starts open. Stack them one under the other; not for a single block that never closes, that is a card.
The section's content: here a sentence, on a record page a table of its fields.
{% include "components/section_card.html" with id="section-card-example" title="General information" body="styleguide/entries/_section_card_body.html" %}
section_heading
A section of a page's content starts with a plain h2 (a subsection with h3), no class; the spacing above and below comes with it, and a heading that opens its container gets no space above.
Sources and licences
Body text is 1rem. A section heading gets 2.5rem above and 0.75rem below, a subsection 1.75rem and 0.5rem.
Licence of the data
Headings take the heading colour and weight from tokens.
A minor heading
An h4 gets 1.25rem above; it is smaller than an h3.
<h2>Sources and licences</h2>
<p>
Body text is 1rem. A section heading gets 2.5rem above and 0.75rem below, a
subsection 1.75rem and 0.5rem.
</p>
<h3>Licence of the data</h3>
<p>Headings take the heading colour and weight from tokens.</p>
<h4>A minor heading</h4>
<p class="mb-0">An h4 gets 1.25rem above; it is smaller than an h3.</p>
table
Any table of records (.table, plus .table-hover when a row is something to look at) or of labels and values (.table-pairs). Round corners and a border only through the .table-container wrapper, which also scrolls a wide table. DataTables draws the same table.
| Table | Topic | Rows |
|---|---|---|
| wind_farm_capacity | Energy supply | 4 |
| heat_demand_2030 | Demand | 8,760 |
| grid_nodes | Grid | 1,204 |
| Finished | 2026-10-12 |
|---|---|
| Reviewer | A reviewer |
| Badge | Gold |
<div class="d-grid gap-3">
<div class="table-container">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">Table</th>
<th scope="col">Topic</th>
<th scope="col">Rows</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="#table">wind_farm_capacity</a></td>
<td>Energy supply</td>
<td>4</td>
</tr>
<tr>
<td><a href="#table">heat_demand_2030</a></td>
<td>Demand</td>
<td>8,760</td>
</tr>
<tr>
<td><a href="#table">grid_nodes</a></td>
<td>Grid</td>
<td>1,204</td>
</tr>
</tbody>
</table>
</div>
<div class="table-container">
<table class="table table-pairs">
<tbody>
<tr>
<th scope="row">Finished</th>
<td>2026-10-12</td>
</tr>
<tr>
<th scope="row">Reviewer</th>
<td>A reviewer</td>
</tr>
<tr>
<th scope="row">Badge</th>
<td>Gold</td>
</tr>
</tbody>
</table>
</div>
</div>
tabs
Switches between views of one thing (panes of a page, or the pages of one area); the current tab is marked, at most one icon link to somewhere else sits at the row's end, and an action never sits in the row.
<nav class="nav tabs mb-4" aria-label="Tabs without an aside link">
<a class="nav-link active"
aria-current="page"
href="#tabs"><i class="fas fa-table" aria-hidden="true"></i> Tables</a>
<a class="nav-link"
href="#tabs"><i class="fas fa-database"
aria-hidden="true"></i> Datasets</a>
<a class="nav-link"
href="#tabs">Reviews <span class="badge badge--info">Early Access</span></a>
<a class="nav-link" href="#tabs">Organizations</a>
</nav>
<nav class="nav tabs" aria-label="Tabs with an aside link">
<a class="nav-link"
href="#tabs"><i class="fas fa-table"
aria-hidden="true"></i> Tables</a>
<a class="nav-link active"
aria-current="page"
href="#tabs"><i class="fas fa-database" aria-hidden="true"></i> Datasets</a>
<a class="nav-link" href="#tabs">Reviews</a>
<a class="nav-link" href="#tabs">Organizations</a>
<a class="tabs__aside"
href="#tabs"
aria-label="Settings"
title="Settings"><i class="fas fa-cog" aria-hidden="true"></i></a>
</nav>
tag
One Tag wherever it appears, in its own colour: static to show it, link to filter by it (or to open its editor where tags are administered), removable in a picker's current selection, toggle in a pick list. Never for a status; that is a badge.
<div class="tag-group">
{% include "components/tag.html" with name="Wind" color="#2e7d32" %}
{% include "components/tag.html" with name="Electricity" color="#f9a825" href="?tags=electricity" title="Filter by Electricity" %}
{% include "components/tag.html" with name="Heat" color="#6a1b9a" remove="heat" %}
{% include "components/tag.html" with name="Hydrogen" color="#00838f" toggle_name="catalogue_tags" toggle_value="hydrogen" checked=True %}
{% include "components/tag.html" with name="Mobility" color="#ad1457" toggle_name="catalogue_tags" toggle_value="mobility" %}
{% include "components/tag.html" with name="A topic, no colour" %}
</div>
toast
Short feedback that must not move the page: an action done in the background, or an error a script ran into. Never the browser's alert(); a script calls window.oep.toast(text, {status, title}). A success goes away by itself, an error stays until closed. Feedback that belongs to the page is an alert.
<div class="d-grid gap-2 justify-content-start">
{% include "components/toast.html" with status="success" text="Your profile was saved." %}
{% include "components/toast.html" with status="info" text="The review was saved as a draft." %}
{% include "components/toast.html" with status="warning" title="Not published" text="The metadata does not name a license yet." %}
{% include "components/toast.html" with status="danger" title="Delete failed" text="The table could not be deleted. Try again later." %}
</div>
toc
The "On this page" list of a long text page (about, the legal pages): its entries are the page's own headings, in order, each linking to its heading's id. Put it first in a .toc-layout, the text in .toc-layout__body: a sticky column on the left from 992px up, above the text below that. One per page; not for short pages or lists. Something that belongs above the list (the terms of use's language switch) goes with it into a .toc-layout__side, which then takes the list's place.
A first section
Running text, as wide as the reading measure. The list beside it repeats this page's headings word for word.
A second section with a longer heading that wraps
A long entry wraps inside its column.
<div class="toc-layout">
{% include "components/toc.html" with entries="styleguide/entries/_toc_entries.html" %}
<div class="toc-layout__body">
<h3 id="toc-example-first">A first section</h3>
<p>
Running text, as wide as the reading measure. The list beside it repeats
this page's headings word for word.
</p>
<h3 id="toc-example-second">A second section with a longer heading that wraps</h3>
<p>A long entry wraps inside its column.</p>
</div>
</div>