Skip to content

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 $config and calls parse-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 to set-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.ico for browser tab icon
  • logo.png for 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: true set 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.ico and logo.png are 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--base and --easy__font-family--heading set 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 icon
  • logo.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: true is set in $theme__map--brand. Without it the :root block inside generate-brand-themes() is never emitted and all logo/background variables remain at their defaults.

Process:

  1. Copy the new files to app/frontend/assets/custom_brand/.
  2. Set brand-enabled: true in $theme__map--brand.
  3. 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:

  1. Copy font files to app/frontend/assets/custom_brand/. Keep the file names aligned with the mixin calls, for example ComicRelief-Regular.ttf.
  2. Import typography mixins:
@use "../../../../design_system/shared/stylesheets/foundations/typography/_abstracts/_typography.mixins" as typo;
  1. Inside @if map.get($theme__map--brand, brand-enabled), load all required font faces using typo.loadFontFace(..., true).
  2. 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: loadFontFace points to both woff2 and ttf sources 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:

  1. Keep map name $theme__map--brand (required by theme generation).
  2. Set brand-enabled: true to output brand CSS variables.
  3. Override the color keys (-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.

config/initializers/03_hooks.rb
EasyInitHelper.register_redmine_view_hooks("ModificationAcme::Hooks")
app/utils/modification_acme/hooks.rb
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.

app/views/modification_acme/_brand_head.html.erb
<% 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 on RAILS_SERVE_STATIC_FILES: 1, which the delivered docker-compose.yml sets for app.
  • The third mount replaces the logo in emails, which sidekiq sends, so it is in both services.
  • Do not mount favicon.ico into custom_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:

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.