Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
98b3ac3
feat(theming): add popupThemeMode schema, registry, and controller su…
MarvNC May 24, 2026
dbc7f21
feat(theming): add dynamic theme CSS loading in display and popup
MarvNC May 24, 2026
7eb7cad
feat(theming): add CSS variables for theme override support
MarvNC May 24, 2026
053ba38
feat(theming): add minimal theme CSS and preload hints
MarvNC May 24, 2026
9b1bfa9
feat(theming): add e-ink theme CSS with high-contrast, no-animation s…
MarvNC May 24, 2026
61cf60f
feat(theming): add theme mode selector to settings and welcome pages
MarvNC May 24, 2026
56633fe
fix(theming): correct e-ink outer chrome, popup non-shadow DOM inject…
MarvNC May 24, 2026
5e2301a
fix(theming): restore blue accent colors in minimal theme for frequen…
MarvNC May 24, 2026
7ad5579
feat(theming): show dictionary names as subtle labels instead of tags…
MarvNC May 24, 2026
925a2c5
fix(theming): add breathing room around dictionary labels in minimal …
MarvNC May 24, 2026
c20ff3a
feat(theming): restyle inflection chains as compact pills in minimal …
MarvNC May 24, 2026
7badcd6
fix(theming): remove unicode arrow from inflection chains, use spacin…
MarvNC May 24, 2026
9dccb4a
fix(theming): fix e-ink double border and reduce minimal mode spacing
MarvNC May 24, 2026
de3de69
fix(theming): remove double border from nested frequency tags in e-in…
MarvNC May 24, 2026
3414e03
refactor(theming): replace dynamic theme CSS injection with static li…
MarvNC May 24, 2026
551729c
docs(theming): add theme creation guide and link from README
MarvNC May 24, 2026
de830d3
refactor(theming): restore registry-driven architecture with dual-con…
MarvNC May 24, 2026
de388b3
fix(theming): address review issues — dropdown timing, tag vars, oute…
MarvNC May 24, 2026
5463043
docs(theming): soften prescriptive tone, explain tradeoffs and bypasses
MarvNC May 24, 2026
4fa6e60
fix(theming): address review issues — outer CSS injection, accessibil…
MarvNC May 24, 2026
20d2e91
fix(theming): address review feedback from PR #2423
MarvNC May 25, 2026
09beed4
fix(theme-revamp): address PR review comments
MarvNC May 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ Yomitan provides powerful features not available in other browser-based dictiona
- Anki Integration
- 🔧 [Anki handlebar templates](./docs/templates.md)
- Advanced Features
- 🎨 [Creating themes](./docs/theming.md)
- Troubleshooting
- 🕷️ [Known browser bugs](./docs/browser-bugs.md)

Expand Down
230 changes: 230 additions & 0 deletions docs/theming.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
# Theming

## Overview

Yomitan supports multiple visual themes ("modes") that change the appearance of popups and the search page. Themes are additive CSS overrides — the base "Classic" theme is always present, and other themes layer on top by overriding CSS variables and adding rules.

The active theme is controlled by the `data-theme-mode` attribute on the `<html>` element (set by JavaScript based on user preference). Theme CSS files use attribute selectors like `:root[data-theme-mode='minimal']` to apply only when active.

## How it works

### Additive architecture

- Classic is the base theme ([`display.css`](../ext/css/display.css), [`material.css`](../ext/css/material.css), etc.)
- All other themes are CSS _overrides_ loaded on top
- Only the active theme's selectors match, so unused themes have zero visual effect
- CSS files are loaded statically via `<link>` tags in HTML (no dynamic injection)

### Activation mechanism

- [`theme-controller.js`](../ext/js/app/theme-controller.js) sets `data-theme-mode` on the root element
- CSS selectors like `:root[data-theme-mode='eink']` activate automatically
- No JavaScript logic needed per-theme

## Creating a theme

### Step 1: Create the CSS file

Create `ext/css/theme-<your-theme-id>.css`. Use this template:

```css
/*
* Copyright (C) 2023-2026 Yomitan Authors
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

/* ===== YOUR THEME NAME ===== */

/* Light mode overrides */
:root[data-theme-mode="your-theme-id"][data-theme="light"] {
--text-color: #000000;
--background-color: #ffffff;
/* override other variables as needed */
}

/* Dark mode overrides */
:root[data-theme-mode="your-theme-id"][data-theme="dark"] {
--text-color: #ffffff;
--background-color: #000000;
/* override other variables as needed */
}

/* Component overrides (works for both light and dark) */
:root[data-theme-mode="your-theme-id"] .tag {
border-radius: 0;
}
```

### Step 2: Register the theme

Add your theme to [`ext/js/data/theme-registry.js`](../ext/js/data/theme-registry.js):

```js
export const themes = [
{
id: "classic",
label: "Classic",
css: null /* base CSS is classic; no override file needed */,
},
// ... existing themes ...
{
id: "your-theme-id",
label: "Your Theme Name",
css: "/css/theme-your-theme-id.css",
},
];
```

### Step 3: Add CSS links to HTML

Add a `<link>` tag to each HTML file that displays popup content:

**[`ext/popup.html`](../ext/popup.html)** — inside `<head>`, with other theme links:

```html
<link rel="stylesheet" type="text/css" href="/css/theme-your-theme-id.css" />
```

**[`ext/search.html`](../ext/search.html)** — same location.

**[`ext/popup-preview.html`](../ext/popup-preview.html)** — same location.

### Step 4: Add to settings schema

Add your theme ID to the `popupThemeMode` enum in [`ext/data/schemas/options-schema.json`](../ext/data/schemas/options-schema.json):

```json
"popupThemeMode": {
"type": "string",
"enum": ["classic", "minimal", "eink", "your-theme-id"],
"default": "minimal"
}
```

### Step 5: Add TypeScript type

Add your theme ID to the union type in [`types/ext/settings.d.ts`](../types/ext/settings.d.ts):

```typescript
export type PopupThemeMode = "classic" | "minimal" | "eink" | "your-theme-id";
```

## CSS patterns

### Variable overrides

The easiest way to theme is overriding CSS custom properties. Key variables:

| Variable | Purpose |
| ---------------------------------------- | ---------------------------------- |
| `--text-color` | Primary text |
| `--text-color-light1` through `--light4` | Muted text (progressively lighter) |
| `--background-color` | Main background |
| `--background-color-light` | Lighter background variant |
| `--background-color-dark1` | Darker background variant |
| `--accent-color` | Primary accent |
| `--link-color` | Link text |
| `--tag-*-background-color` | Tag type backgrounds |
| `--sidebar-background-color` | Sidebar background |
| `--light-border-color` | Subtle borders |
| `--medium-border-color` | Standard borders |
| `--dark-border-color` | Strong borders |

### Component targeting

Target specific components with attribute-qualified selectors:

```css
/* All tags in your theme */
:root[data-theme-mode="your-theme-id"] .tag {
border-radius: 4px;
}

/* Only in light mode */
:root[data-theme-mode="your-theme-id"][data-theme="light"] .entry {
border-bottom: 1px solid #eeeeee;
}
```

### Outer chrome styling

Theme CSS files are loaded in **both** contexts:

1. **Inner** — via static `<link>` in `popup.html` / `search.html` / `popup-preview.html`
2. **Outer** — via dynamic injection by `popup.js` into the parent page's shadow DOM

This means a single theme file contains both inner selectors (`:root[data-theme-mode='...']`) and outer selectors (`iframe.yomitan-popup[data-theme-mode='...']`). Selectors that don't match their current context are harmless no-ops.

For popup iframe styling (borders, shadows, radius), add outer selectors at the **end** of your theme file:

```css
iframe.yomitan-popup[data-theme-mode="your-theme-id"] {
--popup-border-radius: 8px;
--popup-border-width: 1px;
--popup-border-color: #cccccc;
--popup-box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
--popup-background-color: #ffffff;
}
```

## Best practices

### Do

- Use CSS variables for colors — easy to override per mode
- Use `currentColor` for borders to inherit text color
- Target specific selectors (`.tag`, `.entry`) rather than universal `*`
- Test both light and dark modes
- Run `npm run test:css` to validate syntax

### Caution

- **`!important`** — Hard to override and fights the cascade. Use higher-specificity selectors instead. If you must (e.g. killing animations), disable the lint rule with `/* stylelint-disable-next-line declaration-no-important */`.
- **Universal `*`** — Slows rendering on large popups. Target specific classes.
- **HTML structure** — Themes are CSS-only; markup changes require core changes.
- **External fonts/resources** — Blocked by extension CSP. Use system fonts only.

## Testing

After creating your theme:

1. **Lint**: `npm run test:css`
2. **Fast tests**: `npm run test:fast`
3. **Visual**: Reload the extension and switch modes in Settings → Appearance
4. **Search page**: Open the search page and verify styling there too

## Example: Minimal theme

The Minimal theme ([`ext/css/theme-minimal.css`](../ext/css/theme-minimal.css)) demonstrates:

- Light/dark palette overrides
- Tag restyling (rounded pills → subtle backgrounds)
- Dictionary label conversion (colored badges → text labels)
- Inflection chain simplification (icons hidden, compact layout)
- Frequency tag accent colors

## Files reference

| File | Purpose |
| --------------------------------------------------------------------------------- | -------------------------- |
| [`ext/css/theme-*.css`](../ext/css/) | Theme stylesheets |
| [`ext/js/data/theme-registry.js`](../ext/js/data/theme-registry.js) | Theme registry |
| [`ext/data/schemas/options-schema.json`](../ext/data/schemas/options-schema.json) | Settings schema |
| [`types/ext/settings.d.ts`](../types/ext/settings.d.ts) | TypeScript types |
| [`ext/popup.html`](../ext/popup.html) | Popup page HTML |
| [`ext/search.html`](../ext/search.html) | Search page HTML |
| [`ext/popup-preview.html`](../ext/popup-preview.html) | Settings preview HTML |
| [`ext/js/app/theme-controller.js`](../ext/js/app/theme-controller.js) | Theme attribute controller |
| [`ext/js/app/popup.js`](../ext/js/app/popup.js) | Outer theme injection |
12 changes: 9 additions & 3 deletions ext/css/display.css
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,9 @@
--tag-search-background-color: #8a8a91;
--tag-pronunciation-dictionary-background-color: #6640be;

--tag-muted-background-color: #8a8a91;
--tag-accent-background-color: #b6327a;

--sidebar-background-color: #f8f9fa;

--sidebar-button-background-color: transparent;
Expand Down Expand Up @@ -219,6 +222,9 @@
--tag-search-background-color: #69696e;
--tag-pronunciation-dictionary-background-color: #6640be;

--tag-muted-background-color: #69696e;
--tag-accent-background-color: #992a67;

--sidebar-background-color: #282828;

--sidebar-button-background-color: transparent;
Expand Down Expand Up @@ -792,7 +798,7 @@ button.action-button:active:not(:disabled) {

/* Tags */
.tag {
--tag-color: var(--tag-default-background-color);
--tag-color: var(--tag-muted-background-color);

display: inline-flex;
flex-flow: row nowrap;
Expand Down Expand Up @@ -855,7 +861,7 @@ button.action-button:active:not(:disabled) {
border-bottom-right-radius: 0;
}
.tag[data-category=name] {
--tag-color: var(--tag-name-background-color);
--tag-color: var(--tag-accent-background-color);
}
.tag[data-category=expression] {
--tag-color: var(--tag-expression-background-color);
Expand All @@ -879,7 +885,7 @@ button.action-button:active:not(:disabled) {
--tag-color: var(--tag-part-of-speech-background-color);
}
.tag[data-category=search] {
--tag-color: var(--tag-search-background-color);
--tag-color: var(--tag-muted-background-color);
}
.tag[data-category=pronunciation-dictionary] {
--tag-color: var(--tag-pronunciation-dictionary-background-color);
Expand Down
13 changes: 7 additions & 6 deletions ext/css/popup-outer.css
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,22 @@
iframe.yomitan-popup {
all: initial;
font-size: 1px;
background-color: #ffffff;
border: 1em solid #999999;
box-shadow: 0 0 10em rgba(0, 0, 0, 0.5);
background-color: var(--popup-background-color, #ffffff);
border: var(--popup-border-width, 1em) solid var(--popup-border-color, #999999);
box-shadow: var(--popup-box-shadow, 0 0 10em rgba(0, 0, 0, 0.5));
Comment thread
MarvNC marked this conversation as resolved.
border-radius: var(--popup-border-radius, 0);
position: fixed;
resize: none;
visibility: hidden;
z-index: 2147483647;
box-sizing: border-box;
}
iframe.yomitan-popup[data-theme=dark] {
background-color: #1e1e1e;
border-color: #666666;
background-color: var(--popup-background-color, #1e1e1e);
border-color: var(--popup-border-color, #666666);
}
iframe.yomitan-popup[data-outer-theme=dark] {
box-shadow: 0 0 10em rgba(255, 255, 255, 0.5);
box-shadow: var(--popup-box-shadow, 0 0 10em rgba(255, 255, 255, 0.5));
}
iframe.yomitan-popup[data-outer-theme=none] {
box-shadow: none;
Expand Down
Loading
Loading