Theme and custom branding¶
Easy8 generates its colour, font and spacing CSS variables from SASS maps. To brand a deployment, you override a few maps and assets.
Branding is the one change you make outside your modification plugin, in three places:
| Place | Content |
|---|---|
app/frontend/assets/custom_brand/ |
logos, fonts, favicon, mailer logo and login background; the build adds a hash to their URLs |
app/frontend/public/custom_brand/ |
files that need a fixed URL; the build copies them unchanged to /packs/custom_brand/ |
app/frontend/src/shared/stylesheets/easy/themes/_theme--brand.scss |
colours, fonts and which brand files to use |
These files are compiled into the frontend assets. On a source installation, rebuild the assets after every change (RAILS_ENV=production bundle exec rails assets:precompile) and keep a copy of the three places for the next update. A Docker image contains the compiled assets, and files mounted into these three places have no effect. On Docker, follow Branding on Docker instead.
How CSS variables are generated¶
Theme values are SASS maps. Four helpers in app/frontend/src/shared/stylesheets/easy/utils/abstracts/css-variables/_create.scss turn a map into CSS variables:
generate-css-vars($vars, $config)is the entry point. It builds the prefix from$configand callsparse-vars.parse-vars($map, $prefix, $is-first-iteration: true)walks the map recursively. A nested map extends the prefix; a simple value (string, number, boolean) goes toset-css-var.set-css-var($value, $prefix)writes--prefix: value;.build-prefix($prefix, $name, $is-first-iteration)builds the prefix. It uses__as the separator on the first iteration and-afterwards, or no separator when the name marks a subcomponent.
Brand assets overview¶
Default brand files are stored in app/frontend/assets/images/brand/.
CSS variables that reference brand files are defined in app/frontend/src/shared/stylesheets/easy/themes/_brand-assets.scss. The file uses the Vite alias @assets, so Vite resolves and hashes the assets.
| CSS variable | Default file | Usage |
|---|---|---|
--logo-url |
logo.svg |
Main colour logo |
--logo-url--black |
logo--bw.svg |
Black/white logo |
--logo-url--inverted |
logo--inverted.svg |
Inverted/light logo |
--logo-url--mark |
logo--mark.svg |
Symbol-only mark |
--logo-url--mark-black |
logo--mark-bw.svg |
Symbol black/white |
--logo-url--mark-inverted |
logo--mark-inverted.svg |
Symbol inverted |
--logo-url--mutation |
logo--mutation.svg |
Alternate logo |
--login-bg-url |
login-bg--default.webp |
Login background |
In addition to CSS-driven assets, the backend also uses:
favicon.icofor browser tab iconlogo.pngfor mailer inline logo attachment
Client theme customisation¶
This section overrides brand assets for one deployment without changing the default files.
Full rebrand checklist¶
A complete rebrand needs all of the following. Any missing item leaves that part of the UI on the default Easy8 brand.
- [ ]
app/frontend/assets/custom_brand/favicon.ico: browser tab icon (16, 32, 48 px sizes) - [ ]
app/frontend/assets/custom_brand/logo.png: mailer inline logo (320 × 89 px recommended) - [ ]
app/frontend/assets/custom_brand/logo.svg: main colour logo - [ ]
app/frontend/assets/custom_brand/logo--bw.svg: black/white logo - [ ]
app/frontend/assets/custom_brand/logo--inverted.svg: inverted/light logo - [ ]
app/frontend/assets/custom_brand/logo--mark.svg: symbol mark - [ ]
app/frontend/assets/custom_brand/logo--mark-bw.svg: symbol mark black/white - [ ]
app/frontend/assets/custom_brand/logo--mark-inverted.svg: symbol mark inverted - [ ]
app/frontend/assets/custom_brand/logo--mutation.svg: alternate logo - [ ]
app/frontend/assets/custom_brand/login-bg--default.svg: login background (any image format, as long as the CSS variable points to it) - [ ]
brand-enabled: trueset in_theme--brand.scss: required to output any brand CSS variables - [ ] CSS asset variables overridden in
_theme--brand.scss(SVG and image files only;favicon.icoandlogo.pngare picked up automatically) - [ ]
app/frontend/assets/custom_brand/<FontName>.ttf: brand font file (required by fallback source) - [ ]
app/frontend/assets/custom_brand/<FontName>.woff2: brand font file (recommended primary source) - [ ] Brand fonts loaded via
typo.loadFontFace(..., true)in_theme--brand.scss - [ ]
--easy__font-family--baseand--easy__font-family--headingset in_theme--brand.scss - [ ] Brand colour keys (
-main,-front,-inverse) set in$theme__map--brand
Override brand files¶
Put custom brand files in app/frontend/assets/custom_brand/. Create the directory if it does not exist.
Automatically reused files¶
If the following files exist in app/frontend/assets/custom_brand/, they are used automatically:
favicon.ico: browser tab iconlogo.png: mailer inline logo attachment
These filenames must remain unchanged. If either file is not present, the application uses the default version from:
app/frontend/assets/images/brand/
Frontend brand assets (SVG / login background)¶
Other frontend brand assets, such as SVG logos or the login background, must be mapped manually through CSS variables in:
app/frontend/src/shared/stylesheets/easy/themes/_theme--brand.scss
Note: The CSS variable overrides only take effect when
brand-enabled: trueis set in$theme__map--brand. Without it the:rootblock insidegenerate-brand-themes()is never emitted and all logo/background variables remain at their defaults.
Process:
- Copy the new files to
app/frontend/assets/custom_brand/. - Set
brand-enabled: truein$theme__map--brand. - Override the asset variables in
_theme--brand.scss.
Example:
// app/frontend/src/shared/stylesheets/easy/themes/_theme--brand.scss
:root {
--logo-url: url('@assets/custom_brand/logo.svg');
--logo-url--black: url('@assets/custom_brand/logo--bw.svg');
--logo-url--inverted: url('@assets/custom_brand/logo--inverted.svg');
--logo-url--mark: url('@assets/custom_brand/logo--mark.svg');
--logo-url--mark-black: url('@assets/custom_brand/logo--mark-bw.svg');
--logo-url--mark-inverted: url('@assets/custom_brand/logo--mark-inverted.svg');
--logo-url--mutation: url('@assets/custom_brand/logo--mutation.svg');
--login-bg-url: url('@assets/custom_brand/login-bg--default.svg');
}
Override brand fonts¶
To use a client-specific font family, place the font files in app/frontend/assets/custom_brand/ and wire them in app/frontend/src/shared/stylesheets/easy/themes/_theme--brand.scss:
- Copy font files to
app/frontend/assets/custom_brand/. Keep the file names aligned with the mixin calls, for exampleComicRelief-Regular.ttf. - Import typography mixins:
@use "../../../../design_system/shared/stylesheets/foundations/typography/_abstracts/_typography.mixins" as typo;
- Inside
@if map.get($theme__map--brand, brand-enabled), load all required font faces usingtypo.loadFontFace(..., true). - Apply the font family globally and set font CSS variables used by the theme.
Example:
@mixin generate-brand-themes() {
@if map.get($theme__map--brand, brand-enabled) {
@include typo.loadFontFace('Comic Relief', 'ComicRelief-Regular', 400, normal, true);
@include typo.loadFontFace('Comic Relief', 'ComicRelief-Bold', 700, normal, true);
html {
font-family: 'Comic Relief', sans-serif !important;
}
:root {
--easy__font-family--base: 'Comic Relief', sans-serif;
--easy__font-family--heading: 'Comic Relief', sans-serif;
}
}
}
Note:
loadFontFacepoints to bothwoff2andttfsources under@assets/custom_brand. Keep both formats when possible for best compatibility and loading performance.
Override theme colour variables¶
Modify app/frontend/src/shared/stylesheets/easy/themes/_theme--brand.scss:
- Keep map name
$theme__map--brand(required by theme generation). - Set
brand-enabled: trueto output brand CSS variables. - Override the
colorkeys (-main,-front,-inverse) as needed.
// app/frontend/src/shared/stylesheets/easy/themes/_theme--brand.scss
@use '../utils/helpers/functions' as *;
@use '../utils/helpers/variables/config' as *;
@use '../utils/abstracts/css-variables/create' as *;
@use 'sass:map';
// This one needs to exist as it is used in the mailer theme
$theme__map--brand: (
brand-enabled: true,
color: (
-main: (
'': prep-color-tints(#75b849),
),
-front: (
'': prep-color-tints(#ffffff),
),
-inverse: (
'': prep-color-tints(#1f2937),
),
),
);
$theme__map--brand-dark: (
color: (
-main: (
'': prep-color-tints(#5f38c3),
),
),
);
@mixin generate-brand-themes() {
@if map.get($theme__map--brand, brand-enabled) {
:root {
@include generate-css-vars($theme__map--brand, map.get($config, global));
}
}
:root[data-theme*='theme-dark'] {
@include generate-css-vars($theme__map--brand-dark, map.get($config, global));
}
}
Dark theme¶
To set values for the Dark theme, create a map named $theme__map--brand-dark in _theme--brand.scss and place
it under the default brand map.
Changes are not reflected in the contrast theme _theme--contrast.scss, which is designed for visually impaired users and optimised for contrast and readability.
Customising account submenu (optional)¶
To add or remove buttons in the account submenu, edit menus.rb:
# plugins/easyproject/easy_plugins/modification_client/lib/modification_client/menus.rb
Redmine::MenuManager.map :account_menu do |menu|
menu.delete :theme_dark_toggle
menu.delete :theme_contrast_toggle
end
Then require menus.rb from an initializer.
# plugins/easyproject/easy_plugins/modification_client/config/initializers/02_menu_manager.rb
Rails.application.config.after_initialize do
require "modification_client/menus"
end
Branding on Docker¶
Your modification plugin adds the branding while Easy8 renders each page: a view hook overrides the CSS variables, and Easy8 serves your brand files from a mounted folder. Nothing is built, and the branding survives Easy8 updates.
It sets the main colour, the logos, the login page background, the font, the favicon and the logo in emails. The login page shows your background over the whole panel, without the Easy8 welcome text. Design system components, such as the primary buttons and the focus frame of form fields, keep the Easy8 colours, the same as with the compiled branding. The dark and high-contrast themes keep their own colours; logos, background and font apply to every theme.
1. Add the branding to the plugin¶
modification_acme/
config/initializers/03_hooks.rb
app/utils/modification_acme/hooks.rb
app/views/modification_acme/_brand_head.html.erb
public/
logo.svg # main logo
logo--inverted.svg # logo on a dark background, such as the top bar
logo--mark.svg # symbol only
login-bg.webp # login page background
favicon.png # browser tab icon
logo.png # logo in emails, 320 × 89 px
Brand-Regular.woff2
Brand-Bold.woff2
Register the hook. If the plugin already has a hooks class, add the render_on line to it.
EasyInitHelper.register_redmine_view_hooks("ModificationAcme::Hooks")
module ModificationAcme
class Hooks < Redmine::Hook::ViewListener
render_on :view_layouts_base_html_head_top, partial: "modification_acme/brand_head"
end
end
The partial goes into the head of every page, including the login page. Set your colour in --brand-main; the lighter and darker tints are computed from it.
<% brand = "#{Redmine::Utils.relative_url_root}/modification_acme" %>
<link rel="icon" type="image/png" href="<%= brand %>/favicon.png">
<style>
@font-face { font-family: "Brand"; src: url("<%= brand %>/Brand-Regular.woff2") format("woff2"); font-weight: 400; }
@font-face { font-family: "Brand"; src: url("<%= brand %>/Brand-Bold.woff2") format("woff2"); font-weight: 700; }
:root:root {
--brand-main: #0f7b5f;
--easy__color--main: var(--brand-main);
--easy__color--main-10: color-mix(in srgb, var(--brand-main) 10%, #fff);
--easy__color--main-20: color-mix(in srgb, var(--brand-main) 20%, #fff);
--easy__color--main-80: color-mix(in srgb, var(--brand-main) 80%, #fff);
--easy__color--main-90: color-mix(in srgb, var(--brand-main) 90%, #fff);
--easy__color--main-110: color-mix(in srgb, var(--brand-main) 90%, #000);
--easy__color--main-120: color-mix(in srgb, var(--brand-main) 80%, #000);
--easy__color--main-180: color-mix(in srgb, var(--brand-main) 20%, #000);
--easy__color--main-190: color-mix(in srgb, var(--brand-main) 10%, #000);
--easy__font-family--base: "Brand", sans-serif;
--easy__font-family--heading: "Brand", sans-serif;
--logo-url: url("<%= brand %>/logo.svg");
--logo-url--black: url("<%= brand %>/logo.svg");
--logo-url--inverted: url("<%= brand %>/logo--inverted.svg");
--logo-url--mutation: url("<%= brand %>/logo.svg");
--logo-url--mark: url("<%= brand %>/logo--mark.svg");
--logo-url--mark-black: url("<%= brand %>/logo--mark.svg");
--logo-url--mark-inverted: url("<%= brand %>/logo--mark.svg");
--login-bg-url: url("<%= brand %>/login-bg.webp");
}
.p-login .f-swiper__slide-text { display: none; }
.p-login .f-swiper__slide-image-wrapper::after { display: block; inset: 0; width: auto; height: auto; background-size: cover; }
</style>
:root:root makes these values win over the defaults, which Easy8 loads later on the page. Leave out any line you do not need; Easy8 then keeps its default for that variable.
2. Mount the plugin and its files¶
Extend the docker-compose.override.yml from Deploy the plugin:
services:
app:
volumes:
- ./modification_acme:/opt/easy/current/plugins/easyproject/easy_plugins/modification_acme:ro
- ./modification_acme/public:/opt/easy/current/public/modification_acme:ro
- ./modification_acme/public/logo.png:/opt/easy/current/app/frontend/assets/custom_brand/logo.png:ro
sidekiq:
volumes:
- ./modification_acme:/opt/easy/current/plugins/easyproject/easy_plugins/modification_acme:ro
- ./modification_acme/public/logo.png:/opt/easy/current/app/frontend/assets/custom_brand/logo.png:ro
Then run docker compose up -d.
- The second mount serves the files at
/modification_acme/. It relies onRAILS_SERVE_STATIC_FILES: 1, which the delivereddocker-compose.ymlsets forapp. - The third mount replaces the logo in emails, which
sidekiqsends, so it is in both services. - Do not mount
favicon.icointocustom_brand: Easy8 then looks for it among the compiled assets and every page fails with HTTP 500. The partial sets the favicon instead. - Easy8 caches views in production. After you change the partial, run
docker compose restart app.
3. Check after each update¶
The variable and class names in the partial are internal to Easy8. If an update renames one, that part of the page shows the Easy8 default again; nothing else breaks. After each update, open the login page and the top bar and compare them with your branding.
Theme key descriptions¶
These keys of the $theme__map--default map (in _theme--default.scss) influence the application's appearance:
Logo¶
The default theme already defines the logo paths; you only override them as described above.
Font¶
The font style is defined in the map. Changing the base font size (key font-size) can affect various dimensions across the application, such as spacing, heights, and widths of individual elements.
Colours¶
Basic colours and their shades for text, background and other elements.
-
Key
-main: The main colour affects the colour scheme of buttons, action elements, hover effects, links, menus, notifications, active elements, the main top bar, and borders of various elements, such as secondary buttons. -
Key
-front: The colour of white backgrounds, module backgrounds, and form elements' backgrounds. This colour often mixes with other colours, such as background colours for menus. -
Key
-inverse: Used for reverse colour logic. It is light for the default theme and dark for the Dark theme. It is used in elements where the colour needs to be adjusted based on the background colour. -
Key
-mixed: Mixes the -front and -background keys, used in specific application parts such as mind maps, sidebars, and tables. -
Key
-text: Influences text throughout the application and contains text categories such as:
muted: Affects the colour of text in disabled form elements, various inactive elements, separators, and some links.\ inverted: Applied in reverse colour logic, similar to inverse, but for text.\ light: Uses the colour from inverted.\ dark: Basic text colour.
Colour palette¶
Key palette: contains the base palette -base and other colour palettes. The values influence multicoloured
components and are interconnected. They can affect elements like trend cards, charts, tables, mind maps, and WBS.
Other elements¶
All components already have all parameters set, such as colours. To customise the components further, use additional keys from the default map and adjust them as needed.
When adjusting various sizes, spacings, and areas around elements, use the functions that convert units from px to rem, so components adapt to the font size:
ms(): defines the basic spacing sizes. The most commonly used values are ms(0) for basic spacing, ms(2) for medium spacing, and ms(-2) for small spacing. The basic scale ranges from -6 to 12.
px(): converts the specified number of pixels to rem and can include negative numbers.
Custom styles¶
Place your own styles in the same file as the maps, at the end of the file.