|
| 1 | +# Theming Hypha |
| 2 | + |
| 3 | +Hypha's interface is built with [Tailwind CSS](https://tailwindcss.com/) and |
| 4 | +[daisyUI](https://daisyui.com/). Every colour, corner radius and border width comes from a small |
| 5 | +set of CSS custom properties, and those properties can be overridden from a single custom |
| 6 | +template — no fork, no frontend build, no recompiled assets. |
| 7 | + |
| 8 | +A handful of overridden values is enough to make Hypha look like it belongs next to your |
| 9 | +organisation's other web properties, without touching a single Hypha template. |
| 10 | + |
| 11 | +Two themes are defined, `light` and `dark`, and visitors switch between them (or follow their |
| 12 | +operating system) with the theme toggle in the footer. Whatever you change, change it in both. |
| 13 | + |
| 14 | +## How the colours are organised |
| 15 | + |
| 16 | +Hypha's defaults live in `hypha/static_src/tailwind/base/themes.css`. You do not need to edit |
| 17 | +that file — but it is worth knowing how it is put together, because it explains why so little |
| 18 | +CSS is needed to re-theme the whole application. |
| 19 | + |
| 20 | +**1. The brand ramp.** A single brand colour, and ten shades derived from it: |
| 21 | + |
| 22 | +```css |
| 23 | +:root { |
| 24 | + --color-brand: oklch(56.2% 0.102 243); |
| 25 | + --color-brand-base: 0.05; |
| 26 | + --color-brand-10: /* … derived from --color-brand … */; |
| 27 | + /* --color-brand-20 through --color-brand-100 */ |
| 28 | +} |
| 29 | +``` |
| 30 | + |
| 31 | +The shades are generated from `--color-brand` with CSS relative colour syntax, so one colour |
| 32 | +produces a consistent light-to-dark ramp. |
| 33 | + |
| 34 | +**2. The two daisyUI themes.** Each theme sets its own colours, mostly daisyUI defaults, with the |
| 35 | +primary colour taken from the brand ramp — a lighter step in dark mode, a slightly darker one in |
| 36 | +light mode: |
| 37 | + |
| 38 | +```css |
| 39 | +@plugin "daisyui/theme" { |
| 40 | + name: "dark"; |
| 41 | + --color-primary: var(--color-brand-50); |
| 42 | + /* … */ |
| 43 | +} |
| 44 | + |
| 45 | +@plugin "daisyui/theme" { |
| 46 | + name: "light"; |
| 47 | + --color-primary: var(--color-brand-60); |
| 48 | + /* … */ |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +The important consequence: `--color-brand` is defined **once**, outside both themes, so |
| 53 | +overriding that one value re-colours both themes at once. |
| 54 | + |
| 55 | +## What each colour controls |
| 56 | + |
| 57 | +| Variable | Where you see it in Hypha | |
| 58 | +| --- | --- | |
| 59 | +| `--color-primary` | Primary buttons, active navigation underline, link hover colour, primary badges. The most visible brand colour. | |
| 60 | +| `--color-primary-content` | Text and icons drawn *on top of* the primary colour. Must contrast with `--color-primary`. | |
| 61 | +| `--color-secondary` | Secondary buttons and badges. | |
| 62 | +| `--color-base-100` | Page and card backgrounds. | |
| 63 | +| `--color-base-200` | Panel, table header and sidebar backgrounds. | |
| 64 | +| `--color-base-300` | Borders, dividers and loading placeholders. | |
| 65 | +| `--color-base-content` | Body text. | |
| 66 | +| `--color-neutral` | A few high-contrast surfaces. | |
| 67 | +| `--color-info`, `--color-success`, `--color-warning`, `--color-error` | Status badges, alerts and form validation messages. Best left close to the conventional blue/green/amber/red so their meaning stays obvious. | |
| 68 | +| `--color-accent` | Not currently used by any Hypha template — changing it has no visible effect. | |
| 69 | +| `--radius-selector`, `--radius-field`, `--radius-box` | Corner radius of checkboxes/toggles, inputs/buttons, and cards/modals. All `0.25rem` by default. | |
| 70 | +| `--border` | Default border width. | |
| 71 | + |
| 72 | +## Where to put your overrides |
| 73 | + |
| 74 | +Create the file `hypha/templates_custom/includes/head_end.html`. |
| 75 | + |
| 76 | +This template is included at the very end of `<head>`, after Hypha's own stylesheets, and exists |
| 77 | +precisely for additions like this. Anything you declare there wins over the defaults, because it |
| 78 | +comes later in the cascade. Nothing needs to be recompiled and no `collectstatic` run is |
| 79 | +needed — the change takes effect as soon as the file is in place. |
| 80 | + |
| 81 | +Everything below goes inside that file. |
| 82 | + |
| 83 | +## Change the brand colour |
| 84 | + |
| 85 | +For most organisations, this is the whole job: |
| 86 | + |
| 87 | +```html |
| 88 | +<style> |
| 89 | + :root { |
| 90 | + --color-brand: oklch(53.2% 0.106 162.8); /* forest green */ |
| 91 | + } |
| 92 | +</style> |
| 93 | +``` |
| 94 | + |
| 95 | +Both themes pick up the new colour, because both derive their primary from the ramp. Primary |
| 96 | +buttons, the active navigation underline, link hover states and primary badges all change |
| 97 | +together, in light and dark mode. |
| 98 | + |
| 99 | +Some starting points, with the equivalent hex value for reference: |
| 100 | + |
| 101 | +| Brand | Hex | `--color-brand` | |
| 102 | +| --- | --- | --- | |
| 103 | +| Hypha default (blue) | `#2378a8` | `oklch(56.2% 0.102 243)` | |
| 104 | +| Forest green | `#1B7F5A` | `oklch(53.2% 0.106 162.8)` | |
| 105 | +| Teal | `#0F766E` | `oklch(51.1% 0.086 186.4)` | |
| 106 | +| Deep purple | `#6C3FBF` | `oklch(49.3% 0.190 294.6)` | |
| 107 | +| Navy blue | `#1D4ED8` | `oklch(48.8% 0.217 264.4)` | |
| 108 | +| Warm red | `#C8442A` | `oklch(56.9% 0.173 33.1)` | |
| 109 | + |
| 110 | +To convert your own brand colour, paste the hex value into [oklch.com](https://oklch.com/). The |
| 111 | +three numbers are lightness, chroma (saturation) and hue. Keep lightness somewhere around |
| 112 | +`50–60%` so the derived ramp has room to go both lighter and darker; if your brand colour is very |
| 113 | +light or very dark, take its hue and chroma but pick a mid lightness here. |
| 114 | + |
| 115 | +You can also override `--color-brand-base` in the same block. It controls how much colour is left |
| 116 | +at the extreme ends of the ramp: raise it towards `0.1` for more saturated tints and shades, |
| 117 | +lower it towards `0` for more neutral ones. |
| 118 | + |
| 119 | +## Change individual colours |
| 120 | + |
| 121 | +To go further than the brand colour, override the daisyUI variables directly. These are set |
| 122 | +*per theme*, so you must override them per theme too — otherwise your light-mode value leaks into |
| 123 | +dark mode: |
| 124 | + |
| 125 | +```html |
| 126 | +<style> |
| 127 | + /* Light theme */ |
| 128 | + :root, |
| 129 | + [data-theme="light"] { |
| 130 | + --color-primary: oklch(48% 0.11 162.8); |
| 131 | + --color-primary-content: oklch(100% 0 0); |
| 132 | + --color-secondary: oklch(45% 0.04 257); |
| 133 | + } |
| 134 | +
|
| 135 | + /* Dark theme */ |
| 136 | + [data-theme="dark"] { |
| 137 | + --color-primary: oklch(70% 0.13 162.8); |
| 138 | + --color-primary-content: oklch(18% 0.02 162.8); |
| 139 | + --color-secondary: oklch(64% 0.03 229); |
| 140 | + } |
| 141 | +</style> |
| 142 | +``` |
| 143 | + |
| 144 | +Dark mode normally wants a lighter, less saturated version of the same colour, so it does not |
| 145 | +glare against the dark background. |
| 146 | + |
| 147 | +!!! warning |
| 148 | + |
| 149 | + Setting a per-theme variable on `:root` alone applies it to *both* themes. Hypha's own dark |
| 150 | + theme rule has the same specificity but comes earlier in the stylesheet, so your later |
| 151 | + declaration wins even when dark mode is active. `--color-brand` is the exception — it is not |
| 152 | + part of either theme, so `:root` is the correct place for it. |
| 153 | + |
| 154 | +A softer, warmer set of backgrounds is a common second change. It tints every panel, table and |
| 155 | +card in the application: |
| 156 | + |
| 157 | +```html |
| 158 | +<style> |
| 159 | + :root, |
| 160 | + [data-theme="light"] { |
| 161 | + --color-base-100: oklch(99% 0.004 85); /* off-white page background */ |
| 162 | + --color-base-200: oklch(97% 0.006 85); /* panels and table headers */ |
| 163 | + --color-base-300: oklch(93% 0.008 85); /* borders and dividers */ |
| 164 | + } |
| 165 | +</style> |
| 166 | +``` |
| 167 | + |
| 168 | +Squarer or rounder corners are a one-liner, and apply to both themes: |
| 169 | + |
| 170 | +```html |
| 171 | +<style> |
| 172 | + :root, |
| 173 | + [data-theme="light"], |
| 174 | + [data-theme="dark"] { |
| 175 | + --radius-field: 0.5rem; /* inputs and buttons */ |
| 176 | + --radius-box: 0.75rem; /* cards and modals */ |
| 177 | + } |
| 178 | +</style> |
| 179 | +``` |
| 180 | + |
| 181 | +## Larger changes: use a stylesheet file |
| 182 | + |
| 183 | +Once you have more than a few declarations, move the CSS out of the template and into a file. |
| 184 | +Anything under the `public/` directory is served as a static file, so |
| 185 | +`public/css/theme-custom.css` can be linked from the same `head_end.html`: |
| 186 | + |
| 187 | +{% raw %} |
| 188 | +```html |
| 189 | +{% load static %} |
| 190 | +<link rel="stylesheet" href="{% static 'css/theme-custom.css' %}"> |
| 191 | +``` |
| 192 | +{% endraw %} |
| 193 | + |
| 194 | +Run `collectstatic` after adding or changing that file in production: |
| 195 | + |
| 196 | +```bash |
| 197 | +python manage.py collectstatic --noinput --settings=hypha.settings.production |
| 198 | +``` |
| 199 | + |
| 200 | +## Alternative: edit the theme source and rebuild |
| 201 | + |
| 202 | +If you already run Hypha from your own fork and build the frontend assets as part of your |
| 203 | +deployment, you can change the defaults directly in |
| 204 | +`hypha/static_src/tailwind/base/themes.css` instead. Edit `--color-brand` in the `:root` block, |
| 205 | +or set variables inside the two `@plugin "daisyui/theme"` blocks: |
| 206 | + |
| 207 | +```css |
| 208 | +@plugin "daisyui/theme" { |
| 209 | + name: "light"; |
| 210 | + /* … */ |
| 211 | + --color-primary: oklch(48% 0.11 162.8); |
| 212 | + --color-primary-content: oklch(100% 0 0); |
| 213 | + --color-base-100: oklch(99% 0.004 85); |
| 214 | + /* … */ |
| 215 | +} |
| 216 | +``` |
| 217 | + |
| 218 | +Then rebuild. In development, `make serve` watches the file and rebuilds as you save. For a |
| 219 | +production deployment: |
| 220 | + |
| 221 | +```bash |
| 222 | +npm run build |
| 223 | +python manage.py collectstatic --noinput --settings=hypha.settings.production |
| 224 | +``` |
| 225 | + |
| 226 | +This is more work to carry across upgrades, but it is the only approach that also covers the 400 |
| 227 | +and 404 error pages, which are standalone templates that do not include `head_end.html`. |
| 228 | + |
| 229 | +## Logo and favicon |
| 230 | + |
| 231 | +Colours are only part of blending in. |
| 232 | + |
| 233 | +**The site logo** needs no code at all. Upload it in Wagtail admin under |
| 234 | +`Settings` -> `System settings`, where you can set a default logo, a separate mobile logo, and |
| 235 | +the URL the logo links to. Without one, Hypha falls back to its own logo. |
| 236 | + |
| 237 | +**The favicons** are static files in `hypha/static_src/images/favicons/`, and the `<link>` tags |
| 238 | +for them are in `base.html`. Replacing them means either swapping those source files and |
| 239 | +rebuilding, or overriding `base.html` itself — |
| 240 | +[realfavicongenerator.net](https://realfavicongenerator.net/) produces the full set. |
| 241 | + |
| 242 | +Surrounding markup such as `hypha/templates/includes/header-logo.html` can be overridden like any |
| 243 | +other template — see [Overriding templates](overriding-templates.md). |
| 244 | + |
| 245 | +## Before you go live |
| 246 | + |
| 247 | +- **Check both themes.** Use the theme toggle in the footer, and check it in a browser set to |
| 248 | + dark mode as well. |
| 249 | +- **Check the contrast.** Most colours have a matching `--color-*-content` for text drawn on top |
| 250 | + of them, and the `base-*` backgrounds share `--color-base-content`. If you darken |
| 251 | + `--color-primary`, its content colour probably needs to be white; if you lighten it, black. |
| 252 | + Aim for a contrast ratio of at least 4.5:1 — [oklch.com](https://oklch.com/) shows the ratio as |
| 253 | + you adjust a colour, and Hypha's [accessibility notes](../../references/accessibility.md) cover |
| 254 | + the wider commitments. |
| 255 | +- **Keep the status colours recognisable.** Applicants and reviewers rely on red meaning |
| 256 | + rejected and green meaning approved. |
| 257 | +- **The Wagtail admin keeps its own look.** These variables style the applicant, reviewer and |
| 258 | + staff interface; the Wagtail CMS admin is styled separately — see |
| 259 | + [Wagtail admin](../../references/wagtail-admin.md). |
0 commit comments