Emoncms CSS guide
How to style pages in emoncms core and modules. Emoncms uses Bootstrap 5.3 with an emoncms theme and a small set of shared components. New pages and modules start from the patterns here.
Converting Bootstrap 2 code: see bootstrap5-migration.md.
Contents:
Overview
Principles
Adding CSS to a module
Colours and tokens
Light and dark
Base styles
Page families
Setup page components
Reference pages
Apps
Bootstrap build
Planned
1. Overview
Every page loads, in this order (Theme/theme.php):
File |
Contents |
|---|---|
|
Bootstrap 5.3.8, a reduced build (see Bootstrap build) |
|
Palette and tokens, element look, buttons, component sizes, page shell, reference page components |
|
Glyphicons ( |
|
Colour schemes, sidebar sets, shell details |
|
Top menu and sidebar |
|
Panel component |
|
Group list and sticky list toolbar |
|
Autocomplete fields |
|
SVG icons ( |
JS: jQuery, Theme/js/emoncms.js (helpers such as list_toolbar()) and Lib/bootstrap5/js/bootstrap.bundle.min.js (Bootstrap with Popper, at the end of the page). Bootstrap plugins have a jQuery bridge, so $(el).modal('show') works.
Apps also load Modules/app/Views/css/app-kit.css.
2. Principles
Use a Bootstrap class or a shared component before writing page CSS. A new shared component goes in this guide.
Colours and shared values come from the variables in
bootstrap5-theme.css, which have a light and a dark set. Page and app CSS do not set fixed colours. Exceptions: a component that sets both its background and its text, such as a pastel badge, and colours on the coloured top bar or the log window.Colours carry meaning: green is fresh or OK, orange is a warning, red is stale or destructive. An energy source has the same colour in every app.
Page CSS goes in a
.cssfile, not a<style>block. Inlinestyleattributes only for values set from JS.Keep CSS small. Remove a rule when its markup goes.
Page titles are
h3.
3. Adding CSS to a module
Load page CSS and JS with
load_css()andload_js()fromcore.php. The loaders add the file time to the URL, so browsers fetch the new file after an update. Do not use<link>or<script>tags with a fixed?v=.Put the stylesheet beside the view, for example
Modules/sync/sync_view.css.Prefix page classes with a short page name (
net-,bk-,cfg-), so they do not clash with Bootstrap or other pages. Avoid Bootstrap names such asmodal-contentoraccordion-bodyfor page classes.Read colours from variables:
var(--bs-primary),var(--bs-border-color),var(--ec-text-muted). A new shared colour goes in both sets of the theme as--ec-*.A Bootstrap class that is new to the codebase may be missing from the reduced build. Run
node scripts/bootstrap5/build.mjs --checkand rebuild if it lists the class (see Bootstrap build).
4. Colours and tokens
All colours and shared values are at the top of bootstrap5-theme.css, in three blocks. Components read them, so a look change is an edit there.
Block |
Contents |
|---|---|
|
Values shared by both modes: button colours, type, shape, spacing, energy colours, and aliases such as |
|
Light set |
|
Dark set. Primary |
The light and dark sets declare the same variables. A new colour goes in both.
Use the Bootstrap name where the role is the same, and the --ec-* name otherwise.
Role |
Variable |
|---|---|
Accent (links, primary buttons, focus, active items) |
|
Accent hover and border, active |
|
Accent tint |
|
Text |
|
Secondary and muted text |
|
Surfaces |
|
Borders |
|
Panel and group list headers |
|
Row hover |
|
Group list |
|
State colours |
|
Coloured buttons |
|
Code |
|
Other components |
|
Energy (apps) |
|
App surfaces |
|
Extra pastel pairs |
|
--border, --bg-body, --font-* and --radius-* are also available.
Colour schemes: emoncms-base.css holds the scheme classes (.theme-*), which set the top menu colours as --bg-menu-top and related variables, and the sidebar sets (.sidebar-dark, .sidebar-light). A component that should follow the scheme reads these, as the sticky list toolbar does with --bg-menu-top-active.
5. Light and dark
Colour mode uses the Bootstrap 5.3 attribute data-bs-theme.
A page or app turns dark with
data-bs-theme="dark"on its root element. Bootstrap and emoncms components inside it follow the variables, so they work in both modes.[data-bs-theme]also sets the text colour, as Bootstrap sets it onbodyonly.The API pages, Network, the app config panel and the dark apps set
dark. Light apps setlight. Setup pages use the default light set.The page background and footer sit outside the page root. The app kit and the reference pages set them to fixed dark values when a dark root is on the page.
To check a component in both modes, render it inside a
data-bs-theme="dark"wrapper.
6. Base styles
The theme keeps the emoncms look where the Bootstrap 5 defaults differ.
Elements:
Headings bold with 10px margins and the emoncms sizes. Links underlined on hover only. 10px paragraph margin.
Lists indent by a 25px margin with no padding.
.navhas no margin.dltop margin,ddbottom margin,legendfloat andhropacity as before the reboot.codeandprestyled from the tokens.The link hover rule uses
a:where(:hover, :focus), so.btn,.nav-linkand.dropdown-itemkeep no underline.Table cells and links without
hrefinherit their colour.
Change the element look in the theme base section, not with utilities on each page.
Differences from stock Bootstrap 5:
form-controlandform-selectare compact: 14px text, 4px 6px padding. Width classes set fixed widths (see Forms)..input-groupis inline and sized to its content. A full width group needsd-flex w-100.Buttons:
btn-defaultis the plain grey button.btn-xsis an extra small size. Coloured buttons use the emoncms palette.btn-outline-primaryis a tint of the accent.Badges:
badgehas the compact label look. Linked badges (a.badge[href]) are darker.Tables have the emoncms cell padding and borders, and row tints (
table-successand the like) without stripes.Alerts have the emoncms padding.
Modals are 560px wide, 10% from the top, body capped at 400px with scrolling, grey footer, full width with a 20px margin below 768px.
hidesetsdisplay: nonewithout!important, so jQuery.show()can undo it. Bootstrap’sd-nonecannot be undone from jQuery.main.content-containerhasdisplay: flow-rootandwidth: auto, as it sits beside the sidebar.
Bootstrap 5 points to keep in mind:
Every element is
box-sizing: border-box. A width or height includes padding and border. Usebox-sizing: content-boxon a rule that needs the width to exclude them.Utilities are
!important. A page rule cannot override them..row > *gets gutter padding. Userow g-0for columns with their own padding..btn-group > .btnisflex: 1 1 auto. Stop buttons stretching with a two class selector. Addtext-nowrapwhere buttons in a narrow group would wrap..dropdown-toggledraws a caret with::after. Hide it where the design has none..badge:emptyis hidden. A badge used as a dot needsdisplay: inline-block.btn-linkis underlined.
7. Page families
Family |
Purpose |
Look |
Examples to copy |
|---|---|---|---|
Setup pages |
Managing things and settings |
Light, grey and white, in the emoncms shell |
Inputs, Feeds (lists), My Account, Post Process, Schedule, Admin (panels) |
Apps |
Dashboards for the household |
Dark or light, chosen per app |
MyElectricFlow (dark), MyHeatpump (light) |
Reference |
API documentation, network setup |
Dark |
API help pages, Network |
The login page (Modules/user/login_block.php) sits outside the families. It is a light card on --bg-body-login, with the logo in a header in --bg-menu-top, so it follows the colour scheme.
A setup page uses one of two layouts under a page header.
List layout. For pages that list things: feeds, inputs, devices. Rows grouped by node or tag, collapsible, with selection and a sticky toolbar. Examples: Inputs, Feeds, Devices, Sync.
Panel layout. For settings, forms and tools. Examples: My Account (key and value rows with inline edit), Post Process and Schedule (panel table and panel form), Email Reports (tabs and switches), Admin info (compact rows, status tags), Backup, Graph.
8. Setup page components
Page header
h3 title on the left, actions and the help link on the right.
<div class="page-header">
<h3>Feeds</h3>
<a href="feed/api">Feed API Help</a>
</div>
page-lead for a line under the header.
Panel page
panel-page on the page root: grey page background, content 1150px wide, bottom padding, 1rem between panels. A page that needs another width sets max-width on main.content-container:has(.its-page). Panels can sit in a Bootstrap grid (row g-3 > col-lg-6).
Panel
Theme/css/panel.css.
<div class="panel">
<div class="panel-header panel-header-static">
<span class="panel-accent"></span>
<span class="panel-name">API keys</span>
</div>
<div class="panel-row">
<div class="row-key">Read key</div>
<div class="row-value">...</div>
<span class="row-action svg-icon-content_copy" title="Copy"></span>
</div>
</div>
Rows sit straight in the panel. Other content goes in panel-body.
Class |
Role |
|---|---|
|
Container |
|
Header row, pointer and hover |
|
Header that only labels the panel, no pointer or hover |
|
Accent bar before the name, |
|
Title |
|
Count or state beside the name. Also works outside a panel header. |
|
Content with padding |
|
Strip of fields and buttons |
|
Key and value row: |
|
Icon action, shown on row hover and always on touch screens |
|
Stacked form in the panel body: |
|
Message in place of an empty table |
|
Uppercase grey column heads, row hover. |
|
Wrapper for a table that scrolls sideways. The page sets the table |
Inline edit replaces the value with the field and Save and Cancel buttons.
Group list
Theme/css/group-list.css. Column widths are set per page on the group-list grid, with data-col on each cell.
Class |
Role |
|---|---|
|
Grid container |
|
One node or tag |
|
Group row, |
|
Arrow in the header select cell |
|
Group name |
|
Collapsible wrapper, |
|
Item row, |
|
Cell |
Status: --status-color on a header or row sets the stripe on its right edge. Time since update in green or red text.
Sticky list toolbar
div.list-toolbar with btn btn-default icon buttons, then the filter field or page actions pushed right with ms-auto. An empty div.list-toolbar-sentinel goes above it. Once both are rendered, the page calls:
list_toolbar(sentinel, '.list-toolbar');
The toolbar sticks under the top menu with a bar in the menu colour (is-sticky).
Forms
Every text input, select and textarea has form-control or form-select. A bare field shows as a plain browser field.
Need |
Markup |
|---|---|
Field with a fixed width |
|
Widths |
|
Full width |
|
Label above |
|
Help text |
|
Label or unit beside a field |
|
Stacked fields |
|
Invalid |
|
Colour |
|
On and off setting |
|
Checkbox and radio |
Native field, drawn in the accent colour with |
Monospace |
|
Width classes set the total width (padding and border included),
display: inline-blockandvertical-align: middle. They apply only together withform-controlorform-select.input-285andinput-545go full width below 768px, except in an input group.Readonly fields keep the grey background.
A page rule that sets a select’s height must set line height to the height less padding and border, or the text sits low.
A page rule such as
.x input[type=text]also reaches fields inside components such as the date picker. Use a child selector.
Date picker
Lib/js/DateTimePicker.js with Theme/css/datetimepicker.css.
Vue:
<date-time-picker v-model="start" @change="reload">inside an.input-group. It renders an input, a calendar button and a dropdown menu as children of the group.Other pages:
DateTimePicker.attach(input, { value, onChange, buttonClass })adds the button and menu after an existing input in an.input-group. The input keeps its id, value and events, and gets achangeevent when a date is applied. ReturnsgetDate()andsetDate(date).setDatedoes not callonChange.Values are local time,
YYYY-MM-DD HH:MM:SS.DateTimePicker.parseandDateTimePicker.formatconvert to and fromDate.The menu uses Popper with fixed positioning, so a scrolling modal body does not clip it.
Modals
<div id="x" class="modal" tabindex="-1" aria-labelledby="xLabel" aria-hidden="true" data-bs-backdrop="static">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 id="xLabel" class="modal-title">Title</h3>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">...</div>
<div class="modal-footer">...</div>
</div>
</div>
</div>
Open and close with
$(el).modal('show')and$(el).modal('hide'). Events areshown.bs.modalandhidden.bs.modal..modalis the full screen overlay and.modal-dialogthe box. Set the width with--bs-modal-widthon the modal. Position and margin go on.modal-dialog, border and radius on.modal-content.JS that reads the box position reads
.modal-content.
Other Bootstrap JS
Collapse:
data-bs-parentgoes on the.collapseelement, not on the toggle.data-bs-toggle="button"togglesactivebefore a page click handler runs, so the handler sees the new state.
Icons
New code uses SVG icons:
<span class="svg-icon-wrench"></span>. They are CSS masks in the text colour, so they follow the mode and the button colour. The set is inTheme/css/svg-icons.css. A missing icon is added there.Glyphicons (
icon-*,icon-white) still work but are to be replaced (see Planned).
9. Reference pages
The API pages, the Network page and the app config panel share one set of components, section 5 of bootstrap5-theme.css, on Bootstrap cards and the dark set. Each page sets data-bs-theme="dark".
Component |
Classes |
|---|---|
Page |
|
Title |
|
Card |
Bootstrap |
Card title |
|
Section heading |
|
Label |
|
Icon circle |
|
Row |
|
Code |
|
Tags |
|
Page CSS keeps what is particular to the page: the API sidebar, parameter grid and chevron (Lib/api_explorer.css), the WiFi signal bars and row parts (network_view.css), the feed grid and value cells of the config panel (appconf.css). A different colour set is made by overriding the --bs-* variables on a wrapper, as the setup wizard does with net-blue.
10. Apps
Apps share one kit, Modules/app/Views/css/app-kit.css, on the shared variables. Light and dark apps use the same card layout, from MyElectricFlow.
An app loads the kit with load_css, wraps its view (section#app-block, config and loader) in div.app-page and sets data-bs-theme="dark" or "light" on it. Light apps also load Lib/fonts/montserrat/montserrat.css, which the kit applies to a light .app-page. App specific CSS goes in a file beside the app. Light apps sit on the grey page background.
<div class="app-page" data-bs-theme="dark">
<section id="app-block" style="display:none">
<div class="app-card">
<nav class="app-card-head">tabs, then app-card-tools: app-status, config nav</nav>
<div class="app-live">label and value per item</div>
</div>
<div class="app-card app-card-body">app-navbar, chart, app-legend</div>
<div class="app-card app-card-body">app-card-caption, app-flow</div>
</section>
appconf include, ajax-loader
</div>
Component |
Classes |
|---|---|
Tabs |
|
Buttons |
Text or toggle button: |
Energy colours |
|
Fields |
|
Tables |
|
Option rows |
|
Card |
|
Status |
|
Live values |
|
Chart toolbar |
|
Legend |
|
Caption |
|
Line under a chart |
|
Category tiles |
|
Option grid |
|
Flow diagram |
|
Colours:
Surfaces, text, borders and accent come from the shared variables, plus
--ec-app-panel-bgand--ec-app-box-bg.Time bar blue:
rgba(var(--ec-app-nav-rgb), a).Energy colours:
--ec-energy-*(see Colours and tokens). Chart series colours are set in each app’s JS.
Config panel: Lib/appconf, shared by every app and dark in all of them. Header with the app name and Launch app, readiness strip, App and About cards, feeds as two-column rows (status circle, key, node, AUTO, DERIVED or REQUIRED tag, click to edit in place), unused optional feeds behind a Show button, kWh flow feeds card, options as rows with switches, Manage rows. The first .lead paragraph of #appconf-description becomes the header line. Classes cfg-* in appconf.css.
Charts: Flot 5 legend panel and tick labels follow the mode inside .app-page. Tick labels are SVG text, so a font option needs fill. Unlabelled series need label: "" to stay out of the legend. Tooltip classes tooltip-title, tooltip-value and tooltip-units have fixed colours, as the tooltip is added to body.
11. Bootstrap build
Lib/bootstrap5/css/bootstrap.min.css is built from the Bootstrap 5.3.8 Sass by scripts/bootstrap5/build.mjs, then purged against the source of core and the modules. It is not the stock file.
Left out: navbar, accordion, breadcrumb, pagination, list group, toasts, popover, carousel, offcanvas and placeholders.
Included in full: the grid at every breakpoint and the utility families without breakpoint variants (
d-*,flex-*, spacing,w-*,h-*,text-*,bg-*,border-*and similar).Included when used: responsive utility variants such as
d-md-flex, and the classes of each component.
A class that is not in the build has no style. After adding Bootstrap classes:
cd scripts/bootstrap5 && npm ci
node build.mjs --check # classes used in the source but missing from the build
node build.mjs # rebuild, then the same check
To use a left out component, uncomment it in scripts/bootstrap5/scss/bootstrap.scss and rebuild. Modules outside the emoncms repos are not scanned. If a class they use is missing, ask for it to be added to the build. See scripts/bootstrap5/README.md.
12. Planned
Glyphicons to SVG icons. Map each glyph name to an
svg-icon-*, adding missing icons, then removebootstrap2-icons.cssand the sprites inTheme/img/.icon-whitecases need a text colour instead.Site wide light or dark mode as a user setting, set on
<html>. Needs the remaining fixed colours in page CSS moved to variables. Largest first: demandshaper, timeofuse2, the profile app, graph view error and editor colours, device dialog, dashboard widget and editor CSS,autocomplete.css.Inline
styleattributes and!important: tidy when a page is next changed.Further work on bringing the different page family styles together into a unified style.