Component under review
ThemeModeButton, plus the dark mode public API surface (src/lib/index.ts, theme.css, setup docs).
Review scope
Findings
| # |
Severity |
Category |
Summary |
Decision |
| 1 |
Critical |
api |
mode-watcher is a required peer dependency while every other runtime library sits in dependencies. Users must install it themselves, and package managers that do not auto install peers fail outright |
|
| 2 |
Critical |
api |
ModeWatcher and toggleMode are not exported from sv5ui, but two docs pages instruct importing them from 'sv5ui' |
|
| 3 |
Medium |
bug |
ThemeModeButton renders a different icon on the server than on the client in dark mode. Svelte deliberately does not repair {@html} mismatches, so the wrong glyph persists after reload |
|
| 4 |
Low |
api |
resetConfig exists in config.ts and is documented, but is not exported from index.ts |
|
| 5 |
Low |
docs |
theme.css claims support for prefers-color-scheme, but the custom variant only matches the class |
|
| 6 |
Low |
a11y |
No themeColors passed, so no <meta name="theme-color">. Mobile browser chrome does not follow the mode |
|
| 7 |
Low |
docs |
The getting started snippet uses <slot />, which is Svelte 4 syntax |
|
Evidence
Every finding below was reproduced by running code, not by reading it.
Finding 1
I packed the library and installed it into a clean project with peer auto install disabled, then tried to resolve the module from the exact path that imports it:
dist/components/ThemeModeButton/ThemeModeButton.svelte:3
import { toggleMode, mode } from "mode-watcher";
resolve 'mode-watcher' -> MODULE_NOT_FOUND
resolve 'bits-ui' -> resolves fine (control case)
bits-ui is the control: it is equally required at runtime and it resolves, because it lives in dependencies. mode-watcher is the only runtime library classified as a required peer.
Finding 2
A type check probe importing the documented names, with a control import that must pass:
Error: Module '"$lib/index.js"' has no exported member 'ModeWatcher'. (ts)
Error: Module '"$lib/index.js"' has no exported member 'toggleMode'. (ts)
Error: Module '"$lib/index.js"' has no exported member 'resetConfig'. (ts)
svelte-check found 3 errors and 0 warnings in 1 file
The control import of Button from the same module passed, which confirms the checker was actually validating.
Finding 3
Server render and client render of the same component, compared directly:
server : mode.current = undefined -> icon = moon, aria-label = "Switch to dark mode"
client : mode.current = 'dark' -> icon = sun, aria-label = "Switch to light mode"
The cause is that the mode resolver returns undefined during SSR, so the light branch is always taken on the server. Both the {@html} icon body and the aria-label diverge.
Proposed direction
- Move
mode-watcher from peerDependencies to dependencies, matching how every other runtime library is treated, so users install nothing extra.
- Add a
ThemeMode component that wraps the mode watcher internally and supplies themeColors from the surface tokens, then re-export toggleMode, setMode, resetMode and mode. This keeps the underlying library an implementation detail, so replacing it later is not a breaking change. Self closing, as a drop in for the current setup line, since a provider shape would give nothing today: the watcher keeps its state in module scope, not in the component tree.
- Fix the icon divergence with CSS rather than a JavaScript branch, and make the accessible name static so it no longer depends on the mode.
- Correct the docs and the theme comment.
Out of scope, recorded for later
Migrating tokens to light-dark() so system mode needs no JavaScript. I measured it and the trade is not worth taking yet:
- Unsupported browsers do not fall back to light, they lose all color. The two declaration fallback trick does not work for custom properties, so staying safe means keeping the duplicate token block and losing the size benefit.
- The media branch needs an explicit
light class to detect an override, but the current default adds no such class, so a manual light choice on a dark OS would wrongly apply dark utilities.
Worth revisiting only alongside dropping the external mode library, when that class is under our control.
Component under review
ThemeModeButton, plus the dark mode public API surface (
src/lib/index.ts,theme.css, setup docs).Review scope
Findings
mode-watcheris a required peer dependency while every other runtime library sits independencies. Users must install it themselves, and package managers that do not auto install peers fail outrightModeWatcherandtoggleModeare not exported fromsv5ui, but two docs pages instruct importing them from'sv5ui'ThemeModeButtonrenders a different icon on the server than on the client in dark mode. Svelte deliberately does not repair{@html}mismatches, so the wrong glyph persists after reloadresetConfigexists inconfig.tsand is documented, but is not exported fromindex.tstheme.cssclaims support forprefers-color-scheme, but the custom variant only matches the classthemeColorspassed, so no<meta name="theme-color">. Mobile browser chrome does not follow the mode<slot />, which is Svelte 4 syntaxEvidence
Every finding below was reproduced by running code, not by reading it.
Finding 1
I packed the library and installed it into a clean project with peer auto install disabled, then tried to resolve the module from the exact path that imports it:
bits-uiis the control: it is equally required at runtime and it resolves, because it lives independencies.mode-watcheris the only runtime library classified as a required peer.Finding 2
A type check probe importing the documented names, with a control import that must pass:
The control import of
Buttonfrom the same module passed, which confirms the checker was actually validating.Finding 3
Server render and client render of the same component, compared directly:
The cause is that the mode resolver returns
undefinedduring SSR, so the light branch is always taken on the server. Both the{@html}icon body and thearia-labeldiverge.Proposed direction
mode-watcherfrompeerDependenciestodependencies, matching how every other runtime library is treated, so users install nothing extra.ThemeModecomponent that wraps the mode watcher internally and suppliesthemeColorsfrom the surface tokens, then re-exporttoggleMode,setMode,resetModeandmode. This keeps the underlying library an implementation detail, so replacing it later is not a breaking change. Self closing, as a drop in for the current setup line, since a provider shape would give nothing today: the watcher keeps its state in module scope, not in the component tree.Out of scope, recorded for later
Migrating tokens to
light-dark()so system mode needs no JavaScript. I measured it and the trade is not worth taking yet:lightclass to detect an override, but the current default adds no such class, so a manual light choice on a dark OS would wrongly apply dark utilities.Worth revisiting only alongside dropping the external mode library, when that class is under our control.