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:
| Scheme | Colors |
|---|---|
| Default | The brand: pine green with a honey accent, neutrals with a slight green-blue tint |
| Rose | Pink |
| Lavender | Purple |
| Mint | Fresh green |
| Peach | Orange |
| Sky | Light 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/:
| Component | Use |
|---|---|
BrandMark | The 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) |
BrandTile | The mark on a rounded tile in the primary color (side menu, organization switcher) |
BrandLockup | Mark + wordmark; the caller passes the product name |
illustrations.tsx | Small 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:
| Token | Value |
|---|---|
--brand-ring | The ring: var(--primary), so it follows every scheme |
--brand-accent | The first quarter: honey, oklch(0.72 0.13 85) (dark mode oklch(0.8 0.13 85)) |
--brand-dot | The centre dot: var(--foreground) |
--brand-panel | The login panel: deep pine in every scheme (oklch(0.38 0.075 168), dark mode oklch(0.27 0.05 170)) |
--brand-panel-foreground | Text on the login panel |
Tailwind exposes them as bg-brand-panel, text-brand-accent and so on.
Typefaces
| Face | Use | Token |
|---|---|---|
| Schibsted Grotesk | Headings (page, card and dialog titles) and the wordmark | --font-heading (class font-heading) |
| Source Sans 3 | All 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
| File | What |
|---|---|
src/app/icon.svg | The favicon: the mark, with its own colors for a dark browser theme |
src/app/apple-icon.png | Apple touch icon (180 × 180): the mark on a pine tile |
public/icon-192.png, public/icon-512.png | Icons for "Add to Home Screen", listed in the web app manifest |
src/app/manifest.ts | The 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:
node scripts/brand/render-icons.mjsThe 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.
| Color | Meaning | Examples |
|---|---|---|
| Green | Good | On Track, Met, Solved, Completed, Active |
| Red | Bad | Off Track, Missed, High priority, Suspended, Revoked, Overdue |
| Amber | Needs attention | Medium priority, Solving, In Progress, an expired API key, the meeting's warning time |
| Blue | Information | Done, Low priority |
| Grey | Neutral | Open, 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:
:rootholds the default scheme (the brand) in light mode,.darkthe same scheme in dark mode. next-themes puts thedarkclass on<html>.- Every other scheme is a
data-schemeattribute 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.
: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:
[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 theColorSchemetype and toCOLOR_SCHEMES, with ahuefor the swatch in the menus:typescriptexport 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 thecolorSchemeenum, otherwise saving it in Settings is refused. Add it to the scheme type insrc/components/settings/settings-form.tsxas well.src/i18n/messages/en.jsonandsk.json: add its name assettings.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
- Internationalization -- Multi-language support
- Dashboard -- See theming in action
- Configuration -- Full configuration reference