A low-chrome theme for OpenWrt's LuCI web interface, with a light/dark preference that is resolved before first paint, so the page never flashes.
![]() |
![]() |
One command. Paste it into your router over SSH:
wget -O /tmp/quiet.ipk https://github.com/ovexro/luci-theme-quiet/releases/latest/download/luci-theme-quiet_all.ipk && opkg install /tmp/quiet.ipkReload the page and you're on Quiet. That's the whole thing — the package registers the theme and selects it, because nothing else would.
If wget complains about certificates
Some minimal builds ship without CA certificates. Either install them —
opkg update && opkg install ca-bundle — or fetch once with
wget --no-check-certificate. The package's own integrity does not depend on
the transport: every release lists a sha256 you can check with
sha256sum /tmp/quiet.ipk.
Uninstall, just as cleanly:
opkg remove luci-theme-quietThat puts you back on Bootstrap before it deletes anything, so removing the theme can never leave you staring at an unstyled page.
Switch back without uninstalling — System → System → Language and Style → Theme, or:
uci set luci.main.mediaurlbase=/luci-static/bootstrap; uci commit luci; rm -f /tmp/luci-indexcache.*Requires luci-base and luci-theme-bootstrap, both of which you already have.
Architecture: all, so it installs on any target. Built against OpenWrt 24.10.x
/ LuCI 26.x.
| Light | Dark |
|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
The light/dark control sits in the top-right corner, and on the login page too.
It has three states, not two — light, dark, and follow the system — so it can
stop having an opinion once you've expressed one. Alt-click returns it to
automatic. The choice persists in localStorage and is applied by an inline
script in <head>, which is why there is no flash of the wrong theme on load.
cascade.css and mobile.css are the stock luci-theme-bootstrap sheets
verbatim, followed by an override layer. Every widget the layer never
mentions keeps upstream's own correct styling — which is the point of the
architecture, and also its one sharp edge (see
the four failure modes).
mobile.css additionally rewrites stock's deprecated max-device-width to
max-width. That is not cosmetic: both of stock's media queries are gated on it,
and max-device-width tracks the physical screen rather than the viewport, so
at 400% browser zoom or in a narrow window on a large display, none of the
responsive rules apply at all. WCAG 2.1 SC 1.4.10, Reflow.
./build.sh # regenerate src/{cascade,mobile}.css and the ?v= token
./build-ipk.sh # -> dist/luci-theme-quiet_<version>_all.ipk + .manifest
./deploy.sh # push loose files over ssh (the fast dev loop)
./leak-check.sh # refuse to publish anything naming your infrastructure
./release.sh 1.0-r2 "what changed"build.sh is the single implementation of the build, and everything else calls
it. A plain cat does not reproduce cascade.css — the two blank lines
between the stock sheet and the layer are load-bearing for every md5 comparison
in the repo. Three scripts each carried their own copy of those concatenations
once; that is how they drift.
The .ipk build is reproducible: the same sources produce the same sha256.
RELEASE_EPOCH in build-ipk.sh is the timestamp for every archive entry — bump
it alongside VERSION. Timestamps are deliberately not taken from file mtimes,
because build.sh rewrites four files on every run and a fresh clone stamps
everything at checkout time.
build-ipk.sh verifies the finished archive against a layout table written out
independently of the install calls that produced it. The duplication is the
point and must not be refactored away: driving both from one list was tried, and
it silently passes a wrong destination and a wrong file mode. Mutation-tested — a
bad mode, a bad destination, a smuggled extra file, a stale or missing ?v=
token, tampered content, and a dropped LICENSE all fail the build.
src/ the theme sources; cascade.css and mobile.css here are BUILT
stock/ upstream's cascade.css and mobile.css, never modified
package/ control.in + the opkg maintainer scripts + uci-defaults
docs/ screenshots used by this page
dist/ built .ipk and its per-file manifest
deploy.sh needs a deploy.conf — copy deploy.conf.example. That file holds
the only router-specific values anywhere in this repo and is gitignored;
build-ipk.sh needs none of them and runs entirely offline.
Each of these shipped a silent, real defect at least once:
- Specificity loss. A more specific vendor rule drops your declaration with no error anywhere. Mirror the vendor's exact selector rather than inventing a shorter one.
- Leftover geometry. When your rule changes a box, every vendor value pinned to the old geometry is now wrong — including properties you never mentioned.
- Disabled mechanism. When the vendor steers behaviour through a utility
class (
.hidden) or a single property (height:0,margin-top:auto), an equal-specificity restatement in your layer silently turns it off. Restate utility classes last and bare. - Deletion. Removing a line from an override layer does not return the element to your other rule — it hands the element to whatever wins next in the whole cascade, and the vendor's variant rules are usually more specific than your base rule. Before deleting, ask what paints it once your line is gone, and answer by measuring.
- Never emit a
<button>before the Log in button. The reusedbootstrap.sysauthview binds both the click handler and the Enter key todocument.querySelector('button'), and the Log in button sits outside the<form>with noform=attribute — so that binding is the only submit path there is.header.utrenders ahead ofsysauth.ut, so a control emitted from the header becomes the first button and login becomes impossible from a browser. The light/dark control on the login page is a<div role="button">for exactly this reason. <main id="maincontent">is a two-file tag pair — it opens inheader.utand closes infooter.ut. Change one and you must change the other.
The templates reuse its menu-bootstrap.js renderer and its bootstrap.sysauth
login view unchanged, in place, rather than copying them. Remove that package and
this theme has no menu and no way to log in.
Apache-2.0. See LICENSE, and NOTICE for exactly which
bytes are upstream's, which were modified and how. Both are installed to
/usr/share/luci-theme-quiet/ on the router.
Derived from luci-theme-bootstrap, part of the
OpenWrt LuCI project.






