Skip to content

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 sections colors, density, shadows, transitions, accessibility and custom, in that order, each as property:value pairs. 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 the dark section, emitted only when that section is non-empty.
  • Top-level keys other than id, label, the six sections and dark are ignored, so a skin may carry description, author or 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: #ffffff on top of it, and the login panel is from-nebula-950 via-nebula-900 to-nebula-800.
  • 300 and 400 must stay legible on those three. They are the defaults for --nebula-sidebar-text and --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)

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:

  • id must match the file name; it is what the Color Scheme select stores.
  • colors carries all eleven steps. Leave one out and that step keeps the indigo default, which shows wherever a utility or a default uses it.
  • custom is 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-bg is #0d9488 in light and the lighter #14b8a6 in 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 their light-dark() pairs from the stylesheet and dark mode is correct without a dark copy. The moment you set one of them in custom, restate it in dark (see the warning in section 1).
  • --nebula-chart-line and --nebula-chart-fill default to ramp step 500, so the dashboard chart is already teal.
  • Add density, shadows or transitions only when the skin is about those; the indigo-compact, amber-large, flat and matrix skins 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.