100 lines
4.6 KiB
Markdown
100 lines
4.6 KiB
Markdown
|
|
# Otter dark mode
|
||
|
|
|
||
|
|
A three-tier dark theme proposal for the [Otter.ai](https://otter.ai) web app, plus a working
|
||
|
|
userscript that gives you the same thing today.
|
||
|
|
|
||
|
|
Otter's web app has no dark mode. Heavy users read transcripts for hours, frequently at
|
||
|
|
night, and a full-white workspace at 11pm is genuinely painful. This is what a dark theme
|
||
|
|
for it should look like, in three shades rather than one, written so that Otter can adopt
|
||
|
|
it directly.
|
||
|
|
|
||
|
|

|
||
|
|
|
||
|
|
## Why three shades instead of one
|
||
|
|
|
||
|
|
"Dark mode" is not one design. A pure black UI that feels wonderful at 2am feels like a hole
|
||
|
|
punched in the room at 11am, and a lifted grey that is pleasant at noon reads as muddy at
|
||
|
|
night. Rather than pick one and be wrong half the time, ship the token set three times and
|
||
|
|
let the user choose. The tiers differ only in surface lightness, with text lightness moving
|
||
|
|
with the surface so contrast stays roughly constant across all three.
|
||
|
|
|
||
|
|
| Tier | Canvas | Best for |
|
||
|
|
|---|---|---|
|
||
|
|
| **Midnight** | `#000000` | OLED panels, late night |
|
||
|
|
| **Dusk** (default) | `#101216` | mixed lighting |
|
||
|
|
| **Slate** | `#1B1F24` | bright rooms, daytime |
|
||
|
|
|
||
|
|
## Measured contrast
|
||
|
|
|
||
|
|
WCAG 2.1 ratios computed from the token values, not estimated.
|
||
|
|
|
||
|
|
| Tier | Canvas | Body text | Secondary | Tertiary | Accent text | Button label |
|
||
|
|
|---|---|---|---|---|---|---|
|
||
|
|
| Midnight | `#000000` | 17.56:1 | 8.75:1 | 4.44:1 | 8.72:1 | 7.78:1 |
|
||
|
|
| Dusk | `#101216` | 15.39:1 | 8.29:1 | 4.38:1 | 8.39:1 | 8.39:1 |
|
||
|
|
| Slate | `#1B1F24` | 14.25:1 | 8.49:1 | 5.21:1 | 8.26:1 | 9.34:1 |
|
||
|
|
|
||
|
|
Every text pair clears AA (4.5:1) for normal-size text. Body and secondary clear AAA (7:1).
|
||
|
|
|
||
|
|
## The two things that catch dark themes
|
||
|
|
|
||
|
|
**The brand blue cannot be reused as text on dark.** `#0b57d0` measures 3.29:1 on Midnight
|
||
|
|
and 2.59:1 on Slate. It is correct as a *fill* (6.39:1 with white text) but unusable as link
|
||
|
|
or accent text on a dark canvas. The tokens split it: `--ot-accent` for text, `--ot-accent-fill`
|
||
|
|
for solid buttons. The brand blue stays untouched in the light theme.
|
||
|
|
|
||
|
|
**Borders are decorative, not text.** `--ot-border` lands around 1.4:1 against the canvas,
|
||
|
|
which is correct. A 1px separator only needs to be perceived, and the 3:1 requirement applies
|
||
|
|
to UI components that carry meaning, not hairline dividers. Focus rings use the accent and
|
||
|
|
clear 3:1.
|
||
|
|
|
||
|
|
## Files
|
||
|
|
|
||
|
|
| File | What it is |
|
||
|
|
|---|---|
|
||
|
|
| [`preview.html`](preview.html) | Self-contained demo. Any browser, no build, no network. |
|
||
|
|
| [`otter-dark.css`](otter-dark.css) | The tokens. Set `data-theme` on `<html>`, read them with `var(--ot-*)`. |
|
||
|
|
| `preview.png` | Screenshot of the demo, three tiers side by side. |
|
||
|
|
| [`userscript/`](userscript) | A Tampermonkey userscript, the stopgap that works today. See below. |
|
||
|
|
|
||
|
|
## Suggested implementation
|
||
|
|
|
||
|
|
1. Add the token block from `otter-dark.css`, scoped to `:root[data-theme]`.
|
||
|
|
2. Replace hard-coded colours in the workspace shell with `var(--ot-*)`, starting with the
|
||
|
|
three surfaces, text and borders. The accent is the only one that needs a real design
|
||
|
|
decision per component.
|
||
|
|
3. Persist the choice in `localStorage`, default it to `prefers-color-scheme`, and expose it
|
||
|
|
in Settings next to the existing display options.
|
||
|
|
4. Set `color-scheme: dark` so native scrollbars, form controls and autofill follow.
|
||
|
|
|
||
|
|
If a full token migration is too large to schedule, the same palettes can be applied as an
|
||
|
|
overlay stylesheet for the workspace shell only. That covers the bulk of the reading surface,
|
||
|
|
which is where the discomfort is.
|
||
|
|
|
||
|
|
## About the userscript
|
||
|
|
|
||
|
|
`userscript/otter-dark-mode.user.js` is a different beast, deliberately. It exists because
|
||
|
|
you cannot ship a fix to someone else's product on a Tuesday afternoon, and waiting for a
|
||
|
|
feature request to land is not a plan. It inverts the page at the compositor and hue-rotates
|
||
|
|
it back, with each shade being the same transform at a different invert strength.
|
||
|
|
|
||
|
|
Two honest caveats:
|
||
|
|
|
||
|
|
- Lifting the black point forces extreme values to clip, so very saturated colours inside
|
||
|
|
images read slightly muted at the lighter shades. Photos are barely affected because they
|
||
|
|
live in the midtones.
|
||
|
|
- The userscript shades are neutral greys, because an invert cannot produce a tinted surface.
|
||
|
|
The proposal's surfaces are slightly blue-tinted. Same idea, different floor.
|
||
|
|
|
||
|
|
The CSS in this repo has neither problem, because nothing in it is a filter.
|
||
|
|
|
||
|
|
## License
|
||
|
|
|
||
|
|
MIT. Take it, adapt it, ship it under your own design system. The tier structure and the
|
||
|
|
accent-versus-fill split are the parts worth keeping; the hex values are interchangeable.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
Built by Christian Sass, Binary Ranch. See [`PROVENANCE.md`](PROVENANCE.md) for the
|
||
|
|
publication record.
|