Skip to content

Repository files navigation

Graphite

Graphite

A LuCI theme for OpenWrt, luci-theme-graphite.

Palette, accent and brand name are changed from the interface with the separate settings page luci-app-graphite.

English · 简体中文 · 繁體中文

RequirementsInstallPalettesSettingsPhoneLayout

Overview

Tip

Changing the palette, accent or brand name from the interface needs luci-app-graphite, released alongside the theme.

Requirements

OpenWrt 24.10 or later — the theme is written against the ucode template dispatcher
Tested on OpenWrt 25.12.5, LuCI branch 26.180
Browser Chrome 119, Safari 16.4, Firefox 128 or later. The stylesheets use oklch(), relative colour syntax, color-mix(), :has() and CSS logical properties, none of which are polyfilled
Depends luci-base

There is no build step. The stylesheets ship as written; LUCI_MINIFY_CSS is turned off in the Makefile because csstidy rewrites the modern selectors and colour functions above into something else rather than failing on them.

Install

luci.mk builds these with PKGARCH:=all, so one build runs on every device whatever its CPU. Take the packages from the latest release and copy them to the router:

# OpenWrt 25.12 and later. --allow-untrusted because these are not signed
# with the release key; install.sh passes it for you.
apk add --allow-untrusted ./luci-theme-graphite-*.apk
apk add --allow-untrusted ./luci-app-graphite-*.apk    # optional: the settings page
apk add --allow-untrusted ./luci-i18n-graphite-*.apk   # optional: translations

# OpenWrt 24.10 and earlier
opkg install ./luci-theme-graphite_*.ipk

Only the theme is required. It reads /etc/config/graphite and works without the settings page; that package exists so the palette can be changed from the interface rather than over ssh. It lives in its own repository, luci-app-graphite.

Building it yourself

Both Makefiles reach luci.mk through ../../luci.mk, which is resolved against the real path. The packages therefore have to live inside the LuCI feed — under package/, or behind a symlink, the include misses and the build fails before it starts.

cp -r luci-theme-graphite <sdk>/feeds/luci/themes/
cp -r luci-app-graphite   <sdk>/feeds/luci/applications/   # from its own repository
cd <sdk>
./scripts/feeds update -i  # rebuild the index, or install finds nothing
./scripts/feeds install -p luci luci-theme-graphite luci-app-graphite
make menuconfig            # LuCI → Themes, LuCI → Applications
make package/luci-theme-graphite/compile package/luci-app-graphite/compile

The packages land in bin/packages/<arch>/luci/.

The theme registers itself as Graphite, GraphiteLight and GraphiteDark, and becomes the default only on a fresh install where no theme has been chosen. An existing choice is never replaced. Select it under System → System → Language and Style.

Palettes

The default is achromatic. The other five are based on published palettes, named for the flavour they use in dark mode; each pairs with that project's own light flavour — Latte for Catppuccin, Day for Tokyo Night. A palette changes only the token layer: no rule outside palettes.css knows which one is active.

Based on, not copied from: the light flavours are published at a text contrast this theme will not ship. Tokyo Night Day puts secondary text at 3.8:1, under the 4.5:1 floor, and Latte's body text at 7:1. Every palette here, in both schemes, is raised to 10:1 for body text and 5.5:1 for secondary. The hues are upstream's; the text lightness is not. checks/contrast.py paints each pair into a canvas and reads the pixels back, so the figures above are measured rather than intended.

Palette Light Dark
Graphite (default)
Catppuccin Frappé
Catppuccin Macchiato
Catppuccin Mocha
Tokyo Night Storm
Tokyo Night

Two more pages per palette — the realtime graphs and a form-heavy page:

Graphite (default) — Realtime and System

Catppuccin Frappé — Realtime and System

Catppuccin Macchiato — Realtime and System

Catppuccin Mocha — Realtime and System

Tokyo Night Storm — Realtime and System

Tokyo Night — Realtime and System

Accent

The accent paints the parts that mark the main action and the current location: the primary button, the focus ring, the active badge, the brand tile in the sidebar and on the login page, and the current item's rail and tint in the sidebar. Everything else stays grey. It is off by default, and those parts then use the palette's own neutrals.

Eight names, answered by each palette in its own values, so the accent always belongs to the set around it: Catppuccin from each flavour's own accent list, Tokyo Night from folke/tokyonight.nvim, Graphite from a set tuned for a grey interface. accent_custom takes a six-digit hex colour and wins over the name under any palette.

Accent Light Dark
Graphite + blue
Catppuccin Mocha + mauve
Tokyo Night + teal
Graphite + custom #e4572e

The four rows above are one interface under four accents, drawn from Graphite's own values, a Catppuccin flavour, Tokyo Night's palette, and a hand-written colour. They are switched on the appearance page in luci-app-graphite.

Progress bars follow the accent too. The figure on a bar is drawn twice: once across the track in the body colour, once inside the fill in the accent's own foreground, clipped by the fill's overflow, so it changes colour character by character at the boundary. LuCI carries that figure only on the outer element's title, where the fill cannot reach it, so progressbar-graphite.js supplies the second copy and keeps it in step with the poll.

The text on an accent is not a fixed colour. Graphite's eight values have their lightness pushed down from measured contrast so white always clears the floor; Catppuccin and Tokyo Night differ too much between their two ends — a dark flavour's accent sits above 0.85 in lightness, where white gives 1.3:1 — so contrast-color() picks black or white from the actual luminance. checks/contrast.py covers six palettes by eight accents by light and dark, 96 combinations, and holds button text at 4.5:1 against its own fill.

Settings

luci-app-graphite adds System → Appearance, which writes /etc/config/graphite. The values are read server-side and apply to every browser that opens the device.

Option Effect
palette Which set of surface colours the interface uses. Empty means Graphite
accent Colour of the primary button, focus ring, active badge and brand tile. One of eight names; empty leaves it off
accent_custom A six-digit hex colour. Wins over accent under any palette
tint_hue Biases every grey surface towards one hue. A hue angle from 0 to 360, or a colour code such as #3b82f6
tint_chroma How far the greys move towards that hue. 0 is neutral; above 0.01 they stop reading as grey and start competing with the status colours, so that is the ceiling

Light and dark are not in this file. They belong to the reader, not to the device: the three buttons at the top right choose light, dark, or follow the system, and the choice is kept in that browser. Collapsing the sidebar to an icon rail is kept the same way.

The sidebar collapses by group. On load only the group holding the current page is open; a click on a group heading opens or closes it, and several may be open at once. On a device with service packages installed the menu reaches seven groups and fifty-nine items, 3196px of content, which does not fit one screen fully expanded. Collapsing does not apply in the icon rail, where there are no headings to click.

Phone

The layout breakpoint is 48rem. Below it the sidebar becomes a drawer, wide tables scroll inside their own container instead of pushing the page sideways, and the save bar stops being sticky.

Light Dark

A second breakpoint at 23.5rem tightens the secondary buttons in the save bar so the primary action keeps its label. Narrower than 320px in Chinese or 348px in English, that label is clipped rather than allowed to wrap the bar onto two lines. Every current phone is wider than both.

Layout

Six stylesheets in htdocs/luci-static/graphite/, loaded in this order. Each one may only use what the ones above it define.

File Layer Owns
tokens.css design system, vendored Every named colour, size, radius and duration. The only file allowed to write a colour literal
components.css design system, vendored Buttons, fields, native select, badges, tables
shell.css design system, vendored Sidebar, top bar, content column
palettes.css project The five palettes. Token values only, no selectors of its own beyond :root[data-palette]
graphite.css project What this theme adds to the design system: the rail, the login page, the brand mark
luci.css translation LuCI's own class names — .cbi-*, .alert-message, #modal_overlay — mapped onto the layers above

The first three are copied byte-for-byte from the design system and must not be edited here; checks/ holds the scripts that say so. A page's own stylesheet loads after all six, which is the ownership boundary: what a page ships with belongs to that page.

The rest:

ucode/template/themes/graphite/   header.ut, footer.ut, sysauth.ut
htdocs/luci-static/resources/     menu-graphite.js, theme-graphite.js
root/etc/uci-defaults/            theme registration
checks/                           the gates each rule fails on

Login

Translations

English is the source language. Simplified Chinese is complete (luci-i18n-graphite-zh-cn, and luci-i18n-graphite-app-zh-cn for the settings page).

Licence

Apache-2.0. Icons are Lucide (ISC); the licence texts and the design system's provenance are in LICENSES/.

About

A LuCI theme for OpenWrt. Grey carries hierarchy, hue is reserved for status. Six palettes, light and dark.

Topics

Resources

Stars

7 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages