Otter's web app ships no dark mode, and heavy users read transcripts for hours at night. This proposes three shades rather than one, so the choice survives changing lighting instead of being wrong half the day. Contents: - otter-dark.css token set, one block per tier, scoped to :root[data-theme] - preview.html self-contained demo, no build, no network - PREVIEW.png rendered screenshot of the demo - userscript/ Tampermonkey stopgap that works today Contrast figures in the README are measured from the token values with the WCAG 2.1 formula, not estimated. The notable finding is that Otter's brand blue #0b57d0 fails as accent text on every dark surface (2.59:1 to 3.29:1) while remaining correct as a solid fill (6.39:1), so the tokens split accent from accent-fill. MIT licensed. No affiliation with AISense Inc. Co-authored-by: CLU <clu@binaryranch.com>
99 lines
4.6 KiB
Markdown
99 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.
|