Skip to content

Theming & Color Schemes ​

EOS Hub has light and dark mode, six color schemes and its own brand: a mark, a wordmark and two typefaces. All colors are CSS custom properties in the oklch color space, which the shadcn/ui components and Tailwind v4 read directly. Light and dark mode come from next-themes.

Dark and Light Mode ​

Three options are available:

  • Light -- light background with dark text
  • Dark -- dark background with light text
  • System -- follows the operating system (the default)

Switch the mode in the user menu at the bottom of the side menu (Theme), or in Settings (Theme, then Save).

The mode is stored in the browser (localStorage, key eos-hub-theme). A small script applies it before the page is drawn, so a dark page never flashes white. Saving it in Settings also keeps it in your profile.

Color Schemes ​

A color scheme changes the primary, accent, surface, side menu and chart colors, in light and in dark mode:

SchemeColors
DefaultThe brand: pine green with a honey accent, neutrals with a slight green-blue tint
RosePink
LavenderPurple
MintFresh green
PeachOrange
SkyLight blue

Switch the scheme in the user menu (Color Scheme) or in Settings (Color Scheme, then Save). The change applies at once.

The choice is stored in a cookie in this browser (colorScheme, valid for a year). The server reads the cookie and renders the page with the scheme already set, so the colors are right from the first frame. Saving it in Settings also keeps it in your profile.

Brand ​

Mark and Wordmark ​

The mark is a ring of four quarters -- the quarterly rhythm -- around a centre dot, one shared direction. The first quarter, top right, is honey. The other three follow the color scheme's primary color, and the dot uses the text color, so the mark fits every scheme and dark mode while the honey quarter stays.

Next to the mark is the wordmark: the product name from the PRODUCT_NAME environment variable (default EOS Hub), set in the heading typeface. A deployment with its own name shows that name everywhere:

  • on the login page, in the side menu, in onboarding, on the AI assistant consent page and on About;
  • in the web app manifest (the name under the icon on a home screen);
  • in AI connectors: the MCP server's title and instructions, and the OAuth resource name.

See Configuration.

The components are in src/components/brand/:

ComponentUse
BrandMarkThe mark as an inline SVG, colored through the brand tokens. Variants: default (on a page background), tile (light on the primary color), inverse (the current text color with the honey quarter, on a dark brand surface), mono (all in the current text color)
BrandTileThe mark on a rounded tile in the primary color (side menu, organization switcher)
BrandLockupMark + wordmark; the caller passes the product name
illustrations.tsxSmall line illustrations for empty lists, drawn from the mark (ring, quarters, dashed arcs)

Brand Tokens ​

The mark and the login panel have their own tokens in src/app/globals.css:

TokenValue
--brand-ringThe ring: var(--primary), so it follows every scheme
--brand-accentThe first quarter: honey, oklch(0.72 0.13 85) (dark mode oklch(0.8 0.13 85))
--brand-dotThe centre dot: var(--foreground)
--brand-panelThe login panel: deep pine in every scheme (oklch(0.38 0.075 168), dark mode oklch(0.27 0.05 170))
--brand-panel-foregroundText on the login panel

Tailwind exposes them as bg-brand-panel, text-brand-accent and so on.

Typefaces ​

FaceUseToken
Schibsted GroteskHeadings (page, card and dialog titles) and the wordmark--font-heading (class font-heading)
Source Sans 3All other text--font-sans

Both are loaded with next/font/google in src/app/layout.tsx, with the latin and latin-ext subsets, so Slovak diacritics render in the brand faces. The fonts are downloaded at build time and served by the app itself; the browser makes no request to Google.

Login Page ​

On wide screens the login page is split in two. On the left is a deep pine panel with the brand, the sentence Run your business on … (the method's name follows the terminology) and a short line about meetings, scorecard, rocks and issues. On the right is the sign-in form. On phones only the form is shown, under the brand.

Onboarding, the No organization yet page and the AI assistant consent page use the same card with the brand on top.

Empty Lists ​

Empty lists show a small illustration drawn from the mark instead of an icon: Rocks, Issues, To-Dos, Meetings, Scorecard, Documents, the Accountability Chart, the People Analyzer, and nothing matches these filters. The illustrations use the text color and the brand tokens, so they work in every scheme and in dark mode. In code, pass one to EmptyState with its illustration prop.

Icons ​

FileWhat
src/app/icon.svgThe favicon: the mark, with its own colors for a dark browser theme
src/app/apple-icon.pngApple touch icon (180 × 180): the mark on a pine tile
public/icon-192.png, public/icon-512.pngIcons for "Add to Home Screen", listed in the web app manifest
src/app/manifest.tsThe web app manifest: name and short name from PRODUCT_NAME, theme color pine

Icon files can't read CSS variables, so their colors are written out as hex values. After changing the mark, update src/app/icon.svg by hand, then regenerate the PNG icons:

bash
node scripts/brand/render-icons.mjs

The script draws the pine tile with Playwright's Chromium (run npx playwright install chromium once if it is missing) and writes the three PNG files.

The documentation site uses the same mark (docs/public/logo.svg) and the brand color.

Status Colors ​

Status colors mean the same thing on every page, in every color scheme, in light and dark mode. A color scheme changes only the primary and accent colors -- never the status colors.

ColorMeaningExamples
GreenGoodOn Track, Met, Solved, Completed, Active
RedBadOff Track, Missed, High priority, Suspended, Revoked, Overdue
AmberNeeds attentionMedium priority, Solving, In Progress, an expired API key, the meeting's warning time
BlueInformationDone, Low priority
GreyNeutralOpen, Pending, Scheduled

Color is never the only signal: every status badge also names the status in text (often with an icon), and Scorecard cells carry a ✓ or ⚠ icon. A status reads the same for people who don't distinguish red from green, and for screen readers.

Each status color is tuned separately for light and dark mode, so badges stay readable on both backgrounds. The V/TO column bands and the charts keep using the scheme's primary and chart colors.

How It Works ​

All colors are CSS custom properties in oklch, in src/app/globals.css:

  • :root holds the default scheme (the brand) in light mode, .dark the same scheme in dark mode. next-themes puts the dark class on <html>.
  • Every other scheme is a data-scheme attribute on <html> (data-scheme="rose" and so on). The default scheme has no attribute.
  • A scheme block overrides the surface, primary, accent, border, side menu and chart variables, once for light and once for dark.
css
:root {
  --background: oklch(0.995 0.002 170);
  --foreground: oklch(0.2 0.015 200);
  --primary: oklch(0.42 0.085 168);          /* pine */
  --primary-foreground: oklch(0.985 0.005 168);
  --accent: oklch(0.95 0.03 168);
  --chart-1: oklch(0.5 0.09 168);            /* pine */
  --chart-3: oklch(0.72 0.13 85);            /* honey */
  /* ... */
}

.dark {
  --background: oklch(0.17 0.012 200);
  --primary: oklch(0.72 0.1 168);            /* lighter pine */
  --primary-foreground: oklch(0.18 0.03 180);
  /* ... */
}

[data-scheme="rose"] {
  --primary: oklch(0.55 0.15 350);
  /* ... */
}

[data-scheme="rose"].dark,
.dark[data-scheme="rose"] {
  --primary: oklch(0.72 0.12 350);
  /* ... */
}

The @theme inline block at the top of the file maps the variables to Tailwind colors (bg-primary, text-muted-foreground, bg-status-good-bg …), so every component updates when the scheme changes. Charts use var(--chart-1) through var(--chart-5).

Creating a Custom Color Scheme ​

1. Define the CSS Variables ​

In src/app/globals.css, copy an existing scheme (for example Sky) and change its values -- in oklch, one block for light and one for dark mode:

css
[data-scheme="ocean"] {
  --background: oklch(0.985 0.008 200);
  --foreground: oklch(0.2 0.02 200);
  --card: oklch(0.99 0.006 200);
  --card-foreground: oklch(0.2 0.02 200);
  --popover: oklch(0.99 0.006 200);
  --popover-foreground: oklch(0.2 0.02 200);
  --primary: oklch(0.5 0.12 200);
  --primary-foreground: oklch(0.98 0.005 200);
  --secondary: oklch(0.94 0.02 200);
  --secondary-foreground: oklch(0.3 0.04 200);
  --muted: oklch(0.95 0.015 200);
  --muted-foreground: oklch(0.5 0.03 200);
  --accent: oklch(0.93 0.025 200);
  --accent-foreground: oklch(0.25 0.03 200);
  --border: oklch(0.9 0.02 200);
  --input: oklch(0.9 0.02 200);
  --ring: oklch(0.6 0.1 200);
  --sidebar: oklch(0.97 0.015 200);
  --sidebar-foreground: oklch(0.2 0.02 200);
  --sidebar-primary: oklch(0.5 0.12 200);
  --sidebar-primary-foreground: oklch(0.98 0.005 200);
  --sidebar-accent: oklch(0.93 0.025 200);
  --sidebar-accent-foreground: oklch(0.25 0.03 200);
  --sidebar-border: oklch(0.9 0.02 200);
  --chart-1: oklch(0.55 0.13 200);
  --chart-2: oklch(0.6 0.14 20);
  --chart-3: oklch(0.55 0.12 150);
  --chart-4: oklch(0.62 0.13 280);
  --chart-5: oklch(0.65 0.12 60);
}

[data-scheme="ocean"].dark,
.dark[data-scheme="ocean"] {
  --background: oklch(0.16 0.015 200);
  --primary: oklch(0.7 0.1 200);
  --primary-foreground: oklch(0.15 0.02 200);
  /* ... the same variables, tuned for dark mode */
}

Leave the status colors and the brand tokens alone: the mark's ring follows --primary, and the honey quarter and the login panel stay the same in every scheme.

2. Register the Scheme ​

  • src/components/color-scheme-provider.tsx: add the name to the ColorScheme type and to COLOR_SCHEMES, with a hue for the swatch in the menus:

    typescript
    export const COLOR_SCHEMES: { value: ColorScheme; hue: string }[] = [
      { value: "default", hue: "" },
      { value: "rose", hue: "oklch(0.72 0.12 350)" },
      // ...
      { value: "sky", hue: "oklch(0.7 0.1 220)" },
      { value: "ocean", hue: "oklch(0.68 0.1 200)" }, // new
    ]
  • src/app/(dashboard)/settings/actions.ts: add it to the colorScheme enum, otherwise saving it in Settings is refused. Add it to the scheme type in src/components/settings/settings-form.tsx as well.

  • src/i18n/messages/en.json and sk.json: add its name as settings.scheme_ocean.

The profile stores the scheme as text, so no database migration is needed.

3. Test Both Modes ​

Check the new scheme in light and dark mode, on a desktop and on a phone. Pay attention to:

  • text contrast against backgrounds (WCAG AA at least)
  • button states (hover, active, disabled)
  • form inputs and borders
  • chart colors on the dashboard
  • the mark in the side menu and on the login page

TIP

Use a tool like the WebAIM Contrast Checker to verify that your color combinations meet accessibility standards.

Customizing the Default Scheme ​

To change the brand colors without adding a scheme, edit the :root and .dark blocks in src/app/globals.css and the brand tokens (--brand-accent, --brand-panel) further down in the same file. Then update the hex colors in src/app/icon.svg, scripts/brand/render-icons.mjs and src/app/manifest.ts (theme_color), and regenerate the icons.

Next Steps ​