Token and skin reference¶
Every colour, radius, spacing step and shadow in the Nebula admin is a CSS custom property. The theme's stylesheet declares them once with a default for light and dark; a skin is a JSON file that redeclares whichever of them it wants to change. This page is the reference for both halves: the JSON a skin can contain, and the tokens it can set.
For the step-by-step walkthrough of writing and shipping a skin, see Create your own skin. For how a merchant picks one, see Configuration.
1. How a skin is applied¶
A skin is a JSON file at view/adminhtml/nebula/skin/<id>.json in any module.
On every admin page the loader walks every enabled module in load order, reads
each file it finds there, and keeps the ones that carry an id. A later module
that ships a file with the same id replaces the earlier one. A file is read to
at most 256 KiB (262 144 bytes); anything longer is truncated, fails to parse
and is skipped. A missing label becomes ucfirst(id).
The active skin is the value of the config path nebula/theme/skin, set under
Stores › Configuration › Nebula › Nebula Admin Theme › Settings › Color
Scheme. When nothing is stored the default is indigo.
The loader turns the active skin into one string of CSS and the root template
prints it as an inline <style> (with a CSP nonce) immediately after the
<link> to the compiled css/dist/app.css:
/* what the loader emits; one line in the real page */
:root{--color-nebula-50:#eef2ff; … ;--nebula-button-primary-bg:#4f46e5; … ;--nebula-on-primary:#ffffff}
.dark{--nebula-bg-primary:#1f2937; … ;--nebula-button-primary-bg:#6366f1; … ;--nebula-on-primary:#ffffff}
- The
:root{}block is the concatenation of the sectionscolors,density,shadows,transitions,accessibilityandcustom, in that order, each asproperty:valuepairs. The section names are for the author's benefit only; the loader treats all six identically. If the same property appears in two sections the later one wins. - The
.dark{}block is thedarksection, emitted only when that section is non-empty. - Top-level keys other than
id,label, the six sections anddarkare ignored, so a skin may carrydescription,authoror anything else without effect.
Why a skin value overrides the stylesheet¶
base/tokens.css declares every token on :root inside app.css. The skin
also declares on :root. Same selector, same specificity, and the skin's
<style> comes later in the document, so the skin wins. tokens.css is
therefore the value an install sees with no skin selected, not what a merchant
sees. Editing a semantic token in tokens.css changes nothing on an install
with a skin that sets that token, which every shipped skin does for the whole
semantic palette. Tokens a skin leaves alone keep their stylesheet default,
including its light/dark pair.
A skin value has no light/dark pair of its own
The stylesheet writes most colour tokens as light-dark(light, dark). A
skin writes a plain value. When your custom section sets
--nebula-bg-primary: #ffffff, that white replaces the whole
light-dark() pair, so dark mode also gets white cards unless the dark
section restates the token. This is why all 19 shipped skins carry the full
semantic palette in both custom and dark.
The computed foreground¶
A skin picks the colour it fills a primary button with,
--nebula-button-primary-bg. It does not pick what goes on it. As it emits
each block, the loader looks for that property (across the six light sections
in order, and separately in dark), and if the value is a #rgb or #rrggbb
hex it appends --nebula-on-primary set to whichever of #ffffff and #111827
has the higher WCAG contrast ratio against the fill. A value the loader cannot
reason about (var(), oklch(), a gradient) gets no foreground, and the
stylesheet default #ffffff stands. So one property in the JSON yields a
readable pair, and a skin cannot ship a white label on an amber button.
Source: code/Nebula/Skin/Model/SkinLoader.php,
code/Nebula/Skin/Model/Contrast.php,
code/Nebula/Component/ViewModel/Root.php,
design/adminhtml/Nebula/backend/Magento_Theme/templates/root.phtml,
code/Nebula/Core/etc/adminhtml/system.xml.
2. The skin JSON schema¶
| Key | Required | Type | What it does | In the 19 shipped skins |
|---|---|---|---|---|
id |
yes | string | The identifier stored in nebula/theme/skin and the key the loader files the skin under. A file without one is skipped. Use the file name. |
19 of 19 |
label |
no | string | Shown in the Color Scheme select, passed through __(). Defaults to ucfirst(id). |
19 of 19 |
colors |
no | object | The brand ramp, --color-nebula-50 … --color-nebula-950. Emitted into :root{}. |
19 of 19 set all eleven steps |
dark |
no | object | Any property, emitted into .dark{}. This is the only section whose placement differs. |
19 of 19; all set the same 25 semantic tokens (four bg, four text, three border, nine sidebar, header, fieldset header, page, button and primary button); 18 also restate ramp steps 50–900 |
density |
no | object | Spacing, font sizes, radii, sidebar widths. Emitted into :root{}. |
4 of 19 (amber-large, flat, indigo-compact, matrix) |
shadows |
no | object | --nebula-shadow-sm/md/lg. Emitted into :root{}. |
1 of 19 (matrix) |
transitions |
no | object | --nebula-transition-speed. Emitted into :root{}. |
1 of 19 (matrix) |
accessibility |
no | object | The eight status tokens, for skins built around colour vision. Emitted into :root{}. |
3 of 19 (accessible-deuteranopia, accessible-protanopia, high-contrast) |
custom |
no | object | Any property at all. Emitted into :root{} last, so it wins over the other five. |
19 of 19; all set the same 27 tokens (the 25 above plus --nebula-chart-line and --nebula-chart-fill); 18 set --nebula-text-inverted; 16 set the status colours here, 15 of them including the two info tokens (the three accessibility skins set the status colours under accessibility instead) |
Shipped skin files run 3–5 KiB. Values are copied into the CSS verbatim, so
anything valid in a CSS declaration is valid here: hex, rgba(),
color-mix(), var(), a font stack, a shadow list, a length.
Source: code/Nebula/Skin/Model/SkinLoader.php,
code/Nebula/Skin/view/adminhtml/nebula/skin/*.json.
3. The brand ramp¶
tokens.css declares the ramp inside a Tailwind @theme block, which does two
things at once: it defines the --color-nebula-* variables, and it makes
Tailwind generate utilities (bg-nebula-600, text-nebula-700,
border-nebula-200, focus:ring-nebula-500, …) that compile to
var(--color-nebula-*). Because the utilities reference the variable rather than
the hex, a skin's colors section re-colours every template that uses one. A
compatibility layer (base/tailwind-bridge.css) also remaps the literal
indigo-* utilities onto this ramp and the gray-* utilities onto the semantic
tokens, so older templates follow the skin too.
The default ramp is Tailwind's indigo. What each step is for, from where the theme's templates and stylesheets use it:
| Step | Default | Used for |
|---|---|---|
--color-nebula-50 |
#eef2ff |
The lightest tint: bg-nebula-50 and hover:bg-nebula-50 on selected rows and hover surfaces; header and notification tints |
--color-nebula-100 |
#e0e7ff |
bg-nebula-100 chip and selection backgrounds; the dark-mode report and input overrides (dark/reports.css, dark/inputs.css) |
--color-nebula-200 |
#c7d2fe |
text-nebula-200 and border-nebula-200: text and rules on brand-coloured surfaces; menu accents |
--color-nebula-300 |
#a5b4fc |
text-nebula-300; the default --nebula-sidebar-text; --nebula-link in dark mode; fieldset accents |
--color-nebula-400 |
#818cf8 |
text-nebula-400, hover:border-nebula-400; the default --nebula-sidebar-text-muted |
--color-nebula-500 |
#6366f1 |
The busiest step (about 50 utility uses): focus:ring-nebula-500, focus:border-nebula-500, hover:bg-nebula-500, hover:text-nebula-500; the defaults for --nebula-primary-hover, --nebula-input-focus-border, --nebula-input-focus-ring and both chart tokens |
--color-nebula-600 |
#4f46e5 |
The brand's main step: bg-nebula-600 and text-nebula-600 (12 each), accent-nebula-600 on checkboxes; the default --nebula-button-primary-bg and through it --nebula-primary |
--color-nebula-700 |
#4338ca |
bg-nebula-700 and hover:bg-nebula-700 for pressed primary surfaces; text-nebula-700 for a darker accent |
--color-nebula-800 |
#3730a3 |
border-nebula-800, hover:bg-nebula-800/30; the default --nebula-sidebar-border |
--color-nebula-900 |
#312e81 |
via-nebula-900, bg-nebula-900; the default --nebula-sidebar-via and --nebula-sidebar-to; dark brand surfaces in forms |
--color-nebula-950 |
#1e1b4b |
from-nebula-950; the default --nebula-sidebar-from; header and notification dark tints |
600 is a fill step only when the ramp is light-on-dark
A ramp whose 600 is a dark neutral (Slate's is #475569) is fine for text
but wrong as a surface on a dark panel. That is why --nebula-primary falls
back through --nebula-button-primary-bg rather than reading the ramp
directly, and why every skin sets that button token per mode. Ship the ramp
for the utilities; ship the button token for the fill.
Turning one brand colour into eleven steps¶
The ramp does not have to be hand-mixed. Pasting the nearest Tailwind ramp is what the worked example in section 6 does, and the output of any 50–950 palette generator works the same way. Place your brand hex at the step whose job in the table above matches it: 600 if it is a fill colour, 500 if it is an accent. Then build lighter and darker from there.
Six of the steps carry text that the loader's contrast check never sees, since that check protects the primary button's label and nothing else. Test those six by hand:
- 950, 900 and 800 must be dark enough to carry white text. The sidebar
gradient runs from 950 through 900 to 900 with
--nebula-sidebar-text-active: #ffffffon top of it, and the login panel isfrom-nebula-950 via-nebula-900 to-nebula-800. - 300 and 400 must stay legible on those three. They are the defaults for
--nebula-sidebar-textand--nebula-sidebar-text-muted, and the login panel's overlay text. - 600 must clear 4.5:1 against white. Templates use it as text through
text-nebula-600, not only as the primary fill, and no computed foreground covers that use.
Source: design/adminhtml/Nebula/backend/web/css/source/base/tokens.css,
design/adminhtml/Nebula/backend/web/css/source/base/tailwind-bridge.css,
design/adminhtml/Nebula/backend/web/css/source/layout/sidebar.css,
design/adminhtml/Nebula/backend/Magento_Backend/templates/admin/login_branding.phtml.
4. The semantic tokens¶
Every token below is declared in base/tokens.css. "Light default" and "Dark
default" are the two halves of its light-dark() pair, or one value when the
stylesheet declares a single value for both modes. "Used by" names the
stylesheets under web/css/source/ that consume it (counts are var()
references across the 63 source files), so you can tell what a change will move.
The counts are hand-measured and were last taken on 2026-09-19; nothing
regenerates them, so treat a count as an order of magnitude once the stylesheets
have moved.
Tokens marked derived are computed from other tokens. Set the token they are derived from rather than the derived one, unless you have a reason.
Page and surfaces¶
| Token | Light default | Dark default | Used by |
|---|---|---|---|
--nebula-page-bg |
#f9fafb |
#111827 |
The page shell behind everything (layout/main.css) |
--nebula-bg-primary |
#ffffff |
#1f2937 |
Cards, panels, dialogs, grid bodies: media-manager, order-create, forms, dashboard-widgets, dialog, grid (66) |
--nebula-bg-secondary |
#f9fafb |
#111827 |
Secondary panels and grouped areas: dashboard-widgets, media-manager, sales-doc, tree, config/buttons (55) |
--nebula-bg-tertiary |
#f3f4f6 |
#374151 |
The grid header and filter row, inherited config fields, dark-mode config inputs (dark/inputs.css) (32) |
--nebula-bg-hover |
#f9fafb |
#374151 |
Row hover in grids and trees; bridged hover:bg-gray-* (38) |
--nebula-input-bg |
#ffffff |
#374151 |
Inputs, selects and textareas (forms, vendor/form, grid filters, stepper) |
--nebula-neutral-mix |
#171717 |
#f5f5f5 |
The colour hover states mix toward: secondary buttons in vendor/buttons.css mix 8% of it into their surface |
Text¶
| Token | Light default | Dark default | Used by |
|---|---|---|---|
--nebula-text-primary |
#111827 |
#f3f4f6 |
Body copy and headings; bridged text-gray-900/800 (104) |
--nebula-text-secondary |
#374151 |
#d1d5db |
Labels and secondary copy; bridged text-gray-700/600 (103) |
--nebula-text-tertiary |
#636a77 |
#adb4bd |
Hints, grid meta, helper text; bridged text-gray-500 (125) |
--nebula-text-muted |
var(--nebula-text-tertiary) |
same | Placeholders, disabled and de-emphasised text; bridged text-gray-400/300 (44). An alias in the stylesheet because its tertiary already sits near the AA floor; every shipped skin sets a real fourth step |
--nebula-text-inverted |
#ffffff |
#ffffff |
Text on inverted surfaces: the rich-text editor toolbar (web/ts/styles/quill.css) |
--nebula-link derived |
var(--nebula-primary) |
var(--color-nebula-300) |
Links and link-styled actions in grids, forms, menus and the media manager (46). Dark takes the ramp two steps lighter because a mid-ramp accent fails on a near-black page |
--nebula-ink-success / -error / -warning / -info derived |
var(--nebula-color-*) |
color-mix(in srgb, var(--nebula-color-*), white 50%) |
Status colours used as text rather than as a fill: forms, grid, dashboard widgets, metric tiles, and the bridged text-red-600-style utilities (36 across the four) |
--nebula-font-family |
system stack (-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif) |
same | body in base/reset.css. Open Sans is bundled so a skin may opt in; the browser downloads it only when a skin names it |
--nebula-font-size-xs / -sm / -base / -lg / -xl / -2xl |
0.75 / 0.875 / 1 / 1.125 / 1.25 / 1.5rem |
same | xs (102) and sm (96) carry the grid, forms, media manager and sales documents; base is the body size in reset.css; lg dialog titles; 2xl metric tiles and the login page |
Borders¶
| Token | Light default | Dark default | Used by |
|---|---|---|---|
--nebula-border-default |
#e5e7eb |
#4b5563 |
The general border on cards, dialogs, grids and documents (147, the most-used colour token) |
--nebula-border-light |
#f3f4f6 |
#374151 |
Dividers inside forms, grids and menus (26) |
--nebula-border-strong |
#d1d5db |
#6b7280 |
Emphasised edges: the main layout, media gallery, form groups (24) |
--nebula-input-border |
#d1d5db |
#6b7280 |
Input borders (forms, grid filters, vendor/form, stepper, menu) |
--nebula-input-focus-border |
var(--color-nebula-500) |
same | Focused input border |
--nebula-input-focus-ring |
color-mix(in oklab, var(--color-nebula-500) 20%, transparent) |
same | The 2–3px focus ring around inputs and grid inputs |
--nebula-input-radius |
var(--nebula-radius-lg) |
same | Input corner radius (forms, order-create, buttons, config/field) |
Sidebar¶
The stylesheet defaults are ramp references, so a skin that sets only colors
gets a sidebar in its own hue. All 19 shipped skins state them anyway, in both
custom and dark, because the dark sidebar usually wants to be a darker
neutral rather than a darker brand.
| Token | Light default | Dark default | Used by |
|---|---|---|---|
--nebula-sidebar-from |
var(--color-nebula-950) |
same | Top of the sidebar gradient (layout/sidebar.css) |
--nebula-sidebar-via |
var(--color-nebula-900) |
same | Middle of the gradient; also the Magento-style rail background (layout/menu-magento.css) |
--nebula-sidebar-to |
var(--color-nebula-900) |
same | Bottom of the gradient |
--nebula-sidebar-border |
var(--color-nebula-800) |
same | The sidebar's right edge and its section dividers |
--nebula-sidebar-text |
var(--color-nebula-300) |
same | Menu item text |
--nebula-sidebar-text-muted |
var(--color-nebula-400) |
same | Section headings and secondary sidebar text |
--nebula-sidebar-text-active |
#ffffff |
same | Text of the active item and of hovered items |
--nebula-sidebar-active-bg |
rgba(255, 255, 255, 0.15) |
same | Background of the active item |
--nebula-sidebar-hover-bg |
rgba(255, 255, 255, 0.08) |
same | Background of a hovered item |
--nebula-sidebar-width |
16rem |
same | The layout's first grid column and the off-canvas drawer width |
--nebula-sidebar-collapsed-width |
4rem |
same | The same column when sidebar-collapsed is set |
--nebula-rail-width |
8.8rem |
same | Width of the icon-above-label rail when the menu position is "magento" (layout/menu-magento.css) |
--nebula-rail-icon-size |
1.5rem |
same | Rail icon size |
--nebula-rail-label-size |
0.75rem |
same | Rail label size; 11px was the smallest text in the admin and this is the one label a collapsed sidebar shows |
--nebula-rail-item-py |
1rem |
same | Vertical padding of a rail item |
--nebula-rail-flyout-width |
14rem |
same | Width of the rail's flyout submenu |
Header and fieldset¶
| Token | Light default | Dark default | Used by |
|---|---|---|---|
--nebula-header-bg |
#ffffff |
#1f2937 |
The top bar (layout/main.css; one use mixes it to 92% for a translucent sticky bar) |
--nebula-fieldset-header-bg |
rgba(249, 250, 251, 0.8) |
#1f2937 |
Configuration fieldset headers (config/fieldset.css) |
The header's accent utilities in components/header.css (17 uses) are driven by
--nebula-button-primary-bg, mixed with ramp steps 100 and 200 for the lighter
variants, so the top bar's accent follows the primary button rather than a token
of its own.
Buttons¶
| Token | Light default | Dark default | Used by |
|---|---|---|---|
--nebula-button-primary-bg |
var(--color-nebula-600) |
same | The primary fill: the legacy .action-primary surface in vendor/buttons.css, the header accents, array-field and notification accents, forms (48). Also the source of the two derived tokens below and of the computed --nebula-on-primary. This is the one button token a skin must set, per mode |
--nebula-button-bg |
#ffffff |
#374151 |
The secondary and default button surface (vendor/buttons.css, grid toolbar) |
--nebula-primary derived |
var(--nebula-button-primary-bg, var(--color-nebula-600)) |
same | Nebula-native primary buttons and accents in components/buttons.css, media manager, product options, order create, forms, sales documents (89). One skin (magento-classic) sets it directly to keep an accent that is not on its ramp |
--nebula-primary-hover |
var(--color-nebula-500) |
same | Hover of the native primary button (7). No fallback through the button token; a skin whose 500 is not a hover colour should set it |
--nebula-on-primary computed |
#ffffff |
same | The label on the primary fill (components/buttons.css, order-create, forms, product-options, media-manager, sales-doc; 13). Emitted by the loader from --nebula-button-primary-bg; do not set it in a skin |
The legacy .action-primary and .action-secondary labels in vendor/buttons.css
use CSS contrast-color() on their surface instead, so they need no token.
Status colours¶
The stylesheet declares one value per status token with no dark half. Two derivations turn those four hues into everything a reader sees, so badges and chips follow the skin in both modes.
The chip tone contract is the one to check a new palette against.
.nebula-chip--ok, --error, --warning and --info each set a single
variable, --_chip-tone, to the matching --nebula-color-*, and both halves of
the pill derive from it. The background is
color-mix(in srgb, <tone> 14%, transparent), which composites over whatever
surface sits beneath the chip. That is normally the card or grid body,
--nebula-bg-primary, so the same tint lands on white in light mode and on
#1f2937 in dark. The label is deliberately neither the raw hue nor
--nebula-ink-*; it is
light-dark(color-mix(in srgb, <tone>, black 40%), color-mix(in srgb, <tone>, white 30%)).
Why the label is mixed and not the tone itself
A label painted in the same hue as its own 14% tint is bounded from the
start. Measured on the product grid, the four defaults came out at 2.22:1
for success and 1.93:1 for warning in light mode, with error and info near
3.1:1. Mixing the tone 40% toward black in light and 30% toward white in
dark takes all four to between 4.87:1 and 6.93:1 in both themes, with the
tint unchanged. A skin that changes only the four --nebula-color-* hues
keeps a proportional ratio without having to set a second token.
--nebula-ink-success and its three siblings, listed in the text table above,
are the status-as-text tokens. They drive the bridged text-red-600-style
utilities rather than the chip. The rest of base/tailwind-bridge-hues.css
remaps the red/rose, amber/yellow/orange, green/emerald and
blue/sky utilities onto the same four hues as fixed tints: 12% for a -50
background, 18% for a -100 background, 35% for a -200 border or ring, and
45% for a -300 one.
| Token | Light default | Dark default | Used by |
|---|---|---|---|
--nebula-color-success |
#10b981 |
same | Success chips, valid-field states, notifications, metric tiles, tree markers; the bridged green-*/emerald-* utilities (13 direct) |
--nebula-color-success-light |
#ecfdf5 |
same | Reserved. Every skin sets it; the theme's own CSS derives its tints with color-mix() instead, so it is available to modules |
--nebula-color-warning |
#f59e0b |
same | Warning chips, dashboard alerts, form warnings; the bridged amber-*/yellow-*/orange-* utilities (25) |
--nebula-color-warning-light |
#fffbeb |
same | Reserved, as above |
--nebula-color-error |
#ef4444 |
same | Error chips, the danger button in components/buttons.css, validation, dialogs, media manager; the bridged red-*/rose-* utilities (29) |
--nebula-color-error-light |
#fef2f2 |
same | Invalid-row backgrounds in order create and product options (3) |
--nebula-color-info |
#3b82f6 |
same | Info notifications and metric tiles; the bridged blue-*/sky-* utilities (12) |
--nebula-color-info-light |
#eff6ff |
same | Reserved, as above |
Chart¶
| Token | Light default | Dark default | Used by |
|---|---|---|---|
--nebula-chart-line |
var(--color-nebula-500) |
same | The dashboard chart's line. Read from the computed style by Magento_Backend/templates/dashboard/chart.phtml, with #6366f1 as the fallback if the token is empty |
--nebula-chart-fill |
var(--color-nebula-500) |
same | The area under that line, read the same way |
Density: spacing and radius¶
--nebula-spacing-xs through -xl are calc() multiples of the unit, so a skin
that sets only --nebula-spacing-unit rescales all five (amber-large does
this with 0.3rem), while a skin that states all six pins each one
(indigo-compact, matrix).
| Token | Light default | Dark default | Used by |
|---|---|---|---|
--nebula-spacing-unit |
0.25rem |
same | The base unit; layout/main.css pads the page with 5× it |
--nebula-spacing-xs |
1 × unit (0.25rem) |
same | Tight gaps in documents, order create, badges (25) |
--nebula-spacing-sm |
2 × unit (0.5rem) |
same | Gaps in order create, forms, menu, grid, palette (65) |
--nebula-spacing-md |
3 × unit (0.75rem) |
same | The workhorse padding: order create, grid, dialog, sales documents, forms (124) |
--nebula-spacing-lg |
4 × unit (1rem) |
same | Section padding in order create, documents, dialogs, menu (76) |
--nebula-spacing-xl |
6 × unit (1.5rem) |
same | Page and account layout gutters, fieldset spacing (31) |
--nebula-radius-sm |
0.25rem |
same | Small controls, tree nodes, grid chips (22) |
--nebula-radius-md |
0.375rem |
same | Buttons, grid controls, pickers (32) |
--nebula-radius-lg |
0.5rem |
same | Inputs (through --nebula-input-radius), cards, dialogs, media-manager, order-create, forms (52) |
--nebula-radius-xl |
0.75rem |
same | Media tiles, product option cards, metric tiles (14) |
--nebula-radius-2xl |
1rem |
same | Large panels (reset.css, React Spectrum bridge) |
--nebula-radius-full |
9999px |
same | Pills, badges, avatars, toggles (22) |
Shadows¶
light-dark() resolves only <color> values, so shadows cannot use it; the
stylesheet zeroes them with a .dark {} block instead, and a skin's dark
section can do the same or state its own.
| Token | Light default | Dark default | Used by |
|---|---|---|---|
--nebula-shadow-sm |
0 1px 2px 0 rgb(0 0 0 / 0.05) |
none |
Inputs, media tiles, grid cards, file inputs, buttons (21) |
--nebula-shadow-md |
0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1) |
none |
Raised media items, file inputs, hovered buttons (6) |
--nebula-shadow-lg |
0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1) |
none |
Dialogs, menus, dropdowns, the grid's floating panels, config nav (8) |
Transitions¶
| Token | Light default | Dark default | Used by |
|---|---|---|---|
--nebula-transition-speed |
200ms |
same | Every animated state: media manager, tree expand, sidebar collapse, media gallery, menu (65) |
Accessibility¶
accessibility is a JSON section, not a token group. The tokens it carries in
the shipped skins are the eight status colours listed above; it exists so a
reader of the file sees that the status hues were chosen for a colour-vision
condition rather than for a brand. See section 7.
Source: design/adminhtml/Nebula/backend/web/css/source/base/tokens.css and the
stylesheets named in each row, all under
design/adminhtml/Nebula/backend/web/css/source/;
design/adminhtml/Nebula/backend/Magento_Backend/templates/dashboard/chart.phtml;
design/adminhtml/Nebula/backend/web/ts/styles/quill.css.
5. How dark mode is resolved¶
Three things read the mode, and one script keeps them in step. A blocking
script in the <head> of root.phtml runs before first paint: it reads
localStorage key nebula_dark; "true" means dark, "false" means light,
and when the key is absent it falls back to
matchMedia('(prefers-color-scheme: dark)'). If the result is dark it adds the
dark class to <html>. It sets html.style.colorScheme only when a stored
preference exists, because tokens.css declares :root { color-scheme: light
dark }, which already follows the OS when the user has expressed nothing. Once
Alpine boots, the nebula store in partials/runtime-bootstrap.phtml owns the
state: toggleDark() writes nebula_dark, and nebulaSyncRootClasses()
toggles dark on both <html> and <body> (older selectors match
body.dark) and sets colorScheme to dark or light. From there:
light-dark() tokens in tokens.css follow color-scheme; the skin's .dark{}
block and the theme's dark/*.css follow the class; and Tailwind's dark:
utilities follow the class too, through @variant dark (&:where(.dark, .dark *))
in app.css. See Branding and dark mode for the merchant-facing
side.
Source: design/adminhtml/Nebula/backend/Magento_Theme/templates/root.phtml,
design/adminhtml/Nebula/backend/Magento_Theme/templates/partials/runtime-bootstrap.phtml,
design/adminhtml/Nebula/backend/web/css/source/app.css.
6. A starter skin¶
The smallest skin worth shipping sets three things: the ramp (so every utility
and every ramp-referencing default follows it), the sidebar set (so the sidebar
can be a neutral dark rather than a dark brand), and the primary button per mode
(so the fill is a colour chosen for filling, and the label is computed to match).
Everything else keeps its stylesheet default, light and dark. Save it as
view/adminhtml/nebula/skin/teal.json in your module.
{
"id": "teal",
"label": "Teal",
"colors": {
"--color-nebula-50": "#f0fdfa",
"--color-nebula-100": "#ccfbf1",
"--color-nebula-200": "#99f6e4",
"--color-nebula-300": "#5eead4",
"--color-nebula-400": "#2dd4bf",
"--color-nebula-500": "#14b8a6",
"--color-nebula-600": "#0d9488",
"--color-nebula-700": "#0f766e",
"--color-nebula-800": "#115e59",
"--color-nebula-900": "#134e4a",
"--color-nebula-950": "#042f2e"
},
"custom": {
"--nebula-sidebar-from": "#042f2e",
"--nebula-sidebar-via": "#134e4a",
"--nebula-sidebar-to": "#115e59",
"--nebula-sidebar-border": "#0f766e",
"--nebula-sidebar-text": "#99f6e4",
"--nebula-sidebar-text-muted": "#5eead4",
"--nebula-sidebar-text-active": "#ffffff",
"--nebula-sidebar-active-bg": "rgba(94, 234, 212, 0.24)",
"--nebula-sidebar-hover-bg": "rgba(94, 234, 212, 0.14)",
"--nebula-button-primary-bg": "#0d9488"
},
"dark": {
"--nebula-sidebar-from": "#111827",
"--nebula-sidebar-via": "#1f2937",
"--nebula-sidebar-to": "#134e4a",
"--nebula-sidebar-border": "#0f766e",
"--nebula-sidebar-text": "#ccfbf1",
"--nebula-sidebar-text-muted": "#99f6e4",
"--nebula-sidebar-text-active": "#ffffff",
"--nebula-sidebar-active-bg": "rgba(20, 184, 166, 0.3)",
"--nebula-sidebar-hover-bg": "rgba(20, 184, 166, 0.18)",
"--nebula-button-primary-bg": "#14b8a6"
}
}
JSON has no comments, so the notes are here:
idmust match the file name; it is what the Color Scheme select stores.colorscarries all eleven steps. Leave one out and that step keeps the indigo default, which shows wherever a utility or a default uses it.customis used for the sidebar and the button because the section name does not matter to the loader; it is where the shipped skins put them.- The light sidebar runs from the ramp's 950 to its 800, the shipped pattern; the dark one starts from the page's dark neutrals and ends on the ramp so the sidebar does not out-brand a dark page.
--nebula-button-primary-bgis#0d9488in light and the lighter#14b8a6in dark. For both, the loader computes--nebula-on-primary: #111827, since near-black reads at 4.74:1 and 7.13:1 against them where white reads at 3.74:1 and 2.49:1. Pick a darker fill if you want a white label.- Nothing here touches
--nebula-bg-*,--nebula-text-*or--nebula-border-*, so those keep theirlight-dark()pairs from the stylesheet and dark mode is correct without adarkcopy. The moment you set one of them incustom, restate it indark(see the warning in section 1). --nebula-chart-lineand--nebula-chart-filldefault to ramp step 500, so the dashboard chart is already teal.- Add
density,shadowsortransitionsonly when the skin is about those; theindigo-compact,amber-large,flatandmatrixskins are the shipped examples of each.
The tutorial in Create your own skin takes this file through registration, selection and the contrast check.
7. The accessibility skins¶
Three shipped skins use the accessibility section. In each, it holds the same
eight tokens, --nebula-color-success/-warning/-error/-info and their -light
tints, and nothing else. Mechanically the section is emitted into :root{}
like custom; what it changes is the status vocabulary rather than the brand.
accessible-deuteranopia (label "Accessible (Green-Blind / Deuteranopia)")
is for reduced green sensitivity. Its ramp is red (600 = #dc2626) and its
accessibility block moves success off green entirely: success #4d4dff (blue),
error #dc2626 (red), warning #ff7f00 (orange), info #0072b2. Success and
error are then a blue/red pair rather than a green/red pair, with the -light
tints adjusted to match (#eef2ff, #fef2f2, #fff7ed, #e0f2fe).
accessible-protanopia (label "Accessible (Red-Blind / Protanopia)") is for
reduced red sensitivity. Its ramp is blue (600 = #2563eb) and its status set
follows the Okabe–Ito convention: success #0173b2 (blue), error #d55e00
(vermilion), warning #c9ae2c (yellow), info #029e73 (bluish green).
high-contrast is a black-and-yellow scheme, the same in both modes: page
and cards #000000, text #ffff00, primary fill #ffaa00. Its
accessibility block sets the four hues to saturated primaries (#00ff00,
#ffff00, #ff0000, #00ffff) and, because every surface is black, sets the
-light tints to dark versions (#003300, #333300, #330000, #003333)
rather than pale ones. It is also the skin that motivated the computed
foreground: white on #ffaa00 is 1.91:1, near-black is 9.29:1, and the loader
now emits the latter.
Because the theme derives status tints with color-mix() from the base token
and status text through --nebula-ink-*, changing the four base hues in
accessibility re-colours chips, badges, validation states, notifications,
metric tiles and the bridged red-*/green-*/amber-*/blue-* utilities in one
place. A skin for a different condition needs only this section and a ramp whose
steps stay distinguishable under it.
Source: code/Nebula/Skin/view/adminhtml/nebula/skin/accessible-deuteranopia.json,
code/Nebula/Skin/view/adminhtml/nebula/skin/accessible-protanopia.json,
code/Nebula/Skin/view/adminhtml/nebula/skin/high-contrast.json,
code/Nebula/Skin/Model/Contrast.php.