A comprehensive Carbon Design System (v11) theme for Frappe sites. Every surface — desk, website/portal, login, email, charts, and frappe-ui/Espresso components — is restyled to look as though it was designed with Carbon: IBM Plex type, square geometry, Carbon color tokens, 2px focus rings, the g100 UI Shell header, and Carbon's categorical chart palettes, all sourced from the official @carbon/* npm packages.
Workspace — light and dark (data-theme, toggled from Settings → Toggle Theme) share the same tokens, so parity is automatic:
![]() |
![]() |
List view — Carbon data-table header, square status tags, bottom-border filter fields:
Document form — bottom-border fields, Carbon button ramp, sidebar, and the full-width tinted Comments/Activity panel:
Form Builder — the section/field nesting reads as distinct surfaces (canvas → card → field slot → input) instead of one flat gray block:
Dashboard charts — @carbon/charts categorical palette via the carbon_charts.bundle.js wrapper, square stat tiles:
Login — IBM Plex, square fields, Carbon Blue primary action:
Frappe resolves every stylesheet through sites/assets/assets.json, keyed by bare bundle basename. This app shadows frappe's own bundles: it ships same-named entries (desk.bundle.scss, website.bundle.scss, login.bundle.scss, email.bundle.scss) that recompile frappe's SCSS sources with Carbon values injected at compile time, then layer Carbon tokens and component overrides on top. The server is thereby forced to serve the Carbon stylesheets instead of frappe's — one stylesheet per surface, no double download, no cascade fights.
Three mechanisms keep the shadow deterministic:
- Name collision — this app builds after frappe, so its entries win the assets.json merge.
scripts/patch-assets.mjs— runs as the app'sbuildscript after every fullbench build; re-points the shadowed keys (andrtl_variants) at this app's compiled assets and clears the redisassets_jsoncache.after_migratehook (carbon_frappe.build.patch_assets) — re-asserts the shadow after migrations, healing partialbench build --apps frapperuns.
Inside each bundle, the styling itself is a three-layer transposition:
scss/carbon/— emits Carbon's ~240 design tokens as--cds-*custom properties (Light → Carbon g10, Dark → Carbon g100, keyed to frappe'sdata-theme), plus self-hosted IBM Plex@font-facerules.scss/map/— transposes frappe's variable system onto Carbon: the espresso raw color ramps (--gray-*,--blue-*, …) snap to@carbon/colorsscales (which cascades through frappe's semantic--surface-*/--ink-*/--outline-*layer and frappe-ui's identical token names), with exact-token pins for the slots that must land on Carbon theme tokens (--bg-color → --cds-background,--control-bg → --cds-field-02,--btn-primary → blue-60, radii → 0, focus → 2px$focus, elevations → Carbon's single menu shadow, type → IBM Plex).scss/desk/+scss/web/— Carbon component anatomy: bottom-border fields, Carbon button ramps, the g100 UI Shell header, page actions flush on the title row, side-nav selection bars, 48px data tables, Carbon modals/menus/toasts/tags/tiles, and Carbon read-only states.
Why g10 and not White. Carbon's product UIs put the page canvas on
$backgroundand lift cards, tiles and the side nav to$layer-01. The White theme inverts that ladder ($background#ffffff/$layer-01#f4f4f4), which renders as a white page with gray cards — the opposite of every Carbon reference product. g10 gives#f4f4f4/#ffffff. The two themes differ in only 17 of 306 tokens, all in thebackground/layer/field/border-subtlefamily. Noteborder-subtle-00/-01invert between g10 and g100, so all subtle hairlines resolve through an app-level--carbon-border-subtlealias rather than a Carbon token directly.
Charts get --charts-* theming plus carbon_charts.bundle.js, which wraps frappe.Chart and injects the authentic @carbon/charts 14-color categorical palettes whenever a caller doesn't pass explicit colors, and re-themes those charts when data-theme changes (frappe fires no theme event, so this observes the attribute). Chart chrome follows Carbon rather than the concept mockup: gridlines stay (Carbon names the grid as comprehension anatomy) and both axes are IBM Plex Sans.
Three small JS bundles cover what stylesheets cannot reach. Every patch delegates to the original, never throws and never half-applies — a missing target degrades to stock frappe styling and reports itself, rather than throwing inside desk.bundle.js.
carbon_desk.bundle.js— tags formatter output withcarbon-numso numerics and dates get IBM Plex Mono. This is the only hook that reaches frappe-datatable, which emits no fieldtype or alignment class.carbon_anatomy.bundle.js— the Carbon UI Shell header (mounted into the empty<header>frappe leaves inwww/desk.html), the page header (eyebrow +heading-04title, which has no host element in frappe), and 48px report-view rows.scripts/audit-markup.mjs— guards every selector, runtime shape and asset-shadow assumption the above depends on. See Caveats.
bench get-app https://github.com/Avunu/carbon-frappe
bench --site <site> install-app carbon_frappe
bench buildbench get-app installs the npm dependencies (the app-root package.json); the SCSS imports @carbon/styles straight from the app's node_modules. If the first build fails with a missing-import error, run bench setup requirements (or simply re-run bench build) and build again.
sites/assets/assets.json→"desk.bundle.css"should point at/assets/carbon_frappe/dist/css/…- DevTools on
/app:--cds-backgroundpresent on:root;--bg-colorresolves through the--cds-*chain; toggling the theme (Settings → Toggle Theme) flips token values andcolor-scheme. - Fonts: IBM Plex woff2 requests from
/assets/carbon_frappe/fonts/; no Inter requests.
- Bench-global: assets.json is shared by every site on the bench, so the theme applies to sites that don't have the app installed. Run carbon_frappe on a dedicated bench.
- Partial builds:
bench build --apps frappere-points the shadowed keys at frappe's assets until the next fullbench buildorbench migrate. Prefer plainbench build. bench watch: rebuilds of frappe's own bundles re-claim the keys mid-session. This is the most common way to lose the theme, and it has no symptom other than stock frappe styling reappearing — nothing errors.npm run auditnow checks for it explicitly; the fix isnode scripts/patch-assets.mjs.- The shadowing relies on frappe's (undocumented) basename keying and merge semantics of
assets.json. The orthodox fallback —app_include_css/web_include_csshooks — remains structurally compatible with this app's bundles if the shadow mechanism ever breaks.
Both run warn-only on every bench build and strict via npm run audit.
scripts/audit-tokens.mjs— Carbon token renames, removed frappe variables, changes to frappe's bundle entry imports, the g10 premise, and the frappe runtime shapes the theme patches.scripts/audit-markup.mjs— every frappe class the stylesheet targets, every runtime shapejs/anatomy/*wraps, the frappe declarations we deliberately override, and the asset shadow itself. Declared inscripts/markup-manifest.mjsso the guard and the code cannot drift apart.
These exist because all of these failures are silent: the theme keeps loading and a component quietly reverts to stock frappe styling.
Recorded rather than claimed as conformance:
- IBM Plex Mono on tabular data. Carbon is silent on numeric font and alignment in data tables. Monospaced tabular figures are a project choice for financial data; charts deliberately stay Plex Sans, matching Carbon's data-viz references.
Apps that build their own bundles (CRM, Helpdesk, custom portals) can't be reached by the bench pipeline. For those, this app publishes a standalone tokens-only stylesheet — carbon_frappe_ui.bundle.css — that re-themes frappe-ui's semantic variables (unlayered, so it beats Tailwind's @layer base regardless of order) and covers frappe-ui's hardcoded utility classes. Resolve the hashed path via the "carbon_frappe_ui.bundle.css" key in sites/assets/assets.json and include it after the app's own CSS.
- The
email.bundleshadow is Premailer-inlined into outgoing mail — literal values only (no CSS-variable support in that pipeline).
yarn run compile # compile all bundles against a sibling ../frappe checkout (.dev-dist/)
yarn run codegen # refresh vendored IBM Plex fonts + @carbon/charts palettes
yarn run audit # strict drift audit (CI)scripts/dev-compile.mjs replicates frappe's exact sass pipeline (legacy API, includePaths = app roots + node_modules, ~ importer), so bundles can be smoke-tested without a bench. Set FRAPPE_PATH if frappe isn't at ../frappe.
Bump the exact-pinned @carbon/* versions, then npm install && npm run codegen && npm run audit && npm run compile, review the diff, and do a visual pass in both themes.
MIT — see license.txt. IBM Plex is vendored under the OFL (see carbon_frappe/public/fonts/LICENSE-*.txt).






