Skip to content

Design spec

NX Design System

The complete reference for the NX product-family design system: foundations (color, type, space, motion), the token model, the mark, and every shared component. This is the spec; README.md is the install/usage guide and the source files (tokens.css, kit.css, mark.tsx, react.tsx) are the implementation. When they disagree, the source files win — update this doc.

  • One mark, one accent switch. The nx ligature + a neutral foundation are the family constant; each product differs only by its accent hue and wordmark.
  • Token-driven. Every component reads --nx-* custom properties, so a single data-nx-product attribute re-themes an entire subtree.
  • Alpha-aware. Colors are stored as R G B channels, so any token composes with an opacity (rgb(var(--nx-accent-600-rgb) / .7) or Tailwind bg-nx-accent/70).
  • Quiet & flat. Hairline borders and soft accent-tint fills carry hierarchy — no drop shadows.
  • certNX is standalone. It wears the mark + tokens but not the app component kit (see §11).

1. Foundations

1.1 Color — neutrals (shared by every product)

TokenHexR G BRole
--nx-ink#0B0E1411 14 20Primary text, the mark’s n, focus ring
--nx-slate-700#33415551 65 85Strong secondary text
--nx-slate-500#5B677491 103 116Muted text, labels, placeholders
--nx-slate-300#CBD5E1203 213 225Disabled / faint
--nx-hairline#E6E8EC230 232 236Borders, dividers
--nx-paper#F7F8FA247 248 250App background; the mark’s n on dark
--nx-white#FFFFFF255 255 255Card / input surfaces

1.2 Color — per-product accent ramps (partner-approved 2026-07-03)

Each product overrides only the four accent channels; everything else stays shared. The signature is the 600.

Product--nx-accent-50--nx-accent-100--nx-accent-600 (signature)--nx-accent-700 (deep)
ordNX — violet#F5F3FF#EDE9FE#7C3AED#6D28D9
CheckNX — blue (live)#EFF6FF#DBEAFE#2563EB#1D4ED8
PointNX — teal#E6F7F7#C4ECEC#0FA3A3#0A6A6A
certNX — azure (standalone)#DCE8FE#C7DBFD#2F6BEB#1E3A8A
DocLinQ — emerald#ECFDF5#D1FAE5#047857#065F46
FirmNX — ochre#FAF4E8#E8D6B0#7A5C1E#4A3712

Semantic roles of the ramp (how the kit uses each step):

StepAliasUsed for
50--nx-accent-50Section/tile fills (header, KPI), ghost-button hover
100--nx-accent-100Chips/pills bg, row-icon bg, focus-ring glow, hairlines on tinted areas
600--nx-accent / --nx-accent-600Primary buttons, the mark’s x, active underline, links
700--nx-accent-700Text on tint (KPI number, pill/tab text), primary-button hover

Contrast: accent-600 on white and accent-700 on the accent-50/100 tints all meet WCAG AA for the sizes used. Never put accent-600 text on an accent-50 fill for body copy — use 700.

1.3 Typography

TokenStack
--nx-font-sansui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif
--nx-font-monoui-monospace, "Cascadia Code", "SF Mono", "Segoe UI Mono", Menlo, Consolas, monospace

The wordmark and marketing surfaces target Inter where available (see the lockup SVGs); the app kit uses the system sans for zero-latency rendering. The mono layer is for “system”/technical labels (paths, codes, timestamps).

Type scale in use (rem @ 16px base):

NameSizeWeightTrackingWhere
KPI number1.7rem780-0.03em.nx-kpi__num
Lockup word1em (inherits)780-0.02em.nx-lockup__word
Body / control0.875rem620buttons, row titles
Input text0.84rem400.nx-field__input
Tab0.82rem640 (active).nx-tabs
KPI label / sub0.78rem / 0.74rem400labels, list subtext
Field label0.72rem560.nx-field__label
Avatar / pill0.70rem / 0.68rem700 / 640avatar, .nx-pill

Note: weights use non-standard steps (560/620/640/780). With variable fonts they render as-is; with static fonts the browser rounds to the nearest cut (500/600/700).

1.4 Space & radius

Superseded by DS-1. The literal px below predate the DS-1 scale/motion tokens (Task 0) — button/input padding now resolve through the density role tokens, and motion adds —nx-dur-/—nx-ease- alongside —nx-ease. See the DS-1 changelog; DS-2 reconciles the full tables.

  • Radius scale: 8px (row icon) · 10px (button, input) · 12px (KPI) · 18px (card) · 999px (pill). App-icon tiles use 112 on the 512 canvas (≈ 22%).
  • Padding rhythm: card body 18px · header 16px 18px · KPI 14px 16px · input 9px 12px · button 9px 15px. Vertical stack gap inside a card body is 14px; tab gap 18px.

1.5 Motion

Superseded by DS-1 — see the note at the top of §1.4.

TokenValueUse
--nx-easecubic-bezier(.22,.68,.16,1)All kit transitions + the mark draw-on
  • Mark draw-on (.nx-mark.is-playing): the n strokes in over 0.6s, then the accent x follows (0.5s delay). Triggered when the mark scrolls into view.
  • Reduced motion: @media (prefers-reduced-motion: reduce) disables all mark animation and shows the final state. Honor this everywhere.

1.6 Elevation & borders

No shadows. Depth comes from 1px --nx-hairline borders and accent-tint fills (50/100). Cards clip their content (overflow:hidden) so tinted headers meet the rounded corner cleanly.


2. The token model

Tokens live in tokens.css. Two layers:

  1. Channel vars (--nx-*-rgb, e.g. --nx-accent-600-rgb: 37 99 235) — the source of truth. Space-separated RGB so they work with / <alpha>.
  2. rgb() aliases (--nx-accent-600, --nx-accent, …) — convenience, derived from the channels. Declared on both :root and every [data-nx-product] so each themed subtree recomputes its aliases from its own channels.
<!-- the whole theme switch — every nx-* class inside recolours -->
<div data-nx-product="check"> … </div> <!-- "ord" | "check" | "point" | "cert" | "doc" | "firm" -->
<NxThemeProvider product="check">{children}</NxThemeProvider>

Runtime dark surfaces: set --nx-mark-stroke: var(--nx-paper) on a dark wrapper to flip the mark’s n to paper/white (the accent x stays constant).

Full alias list: --nx-ink, --nx-slate-700/500/300, --nx-hairline, --nx-paper, --nx-white, --nx-accent-50/100/600/700, --nx-accent (= 600), --nx-font-sans, --nx-font-mono, --nx-ease, and the runtime --nx-mark-stroke. See Dark theme and Status, chart & overlay tokens (below) for the added --nx-on-accent, --nx-surface-2, --nx-scrim, --nx-shadow-overlay, status (--nx-success/warning/serious/danger) and chart (--nx-chart-1…6) tokens.

Tailwind (optional): tailwind.nx.cjs maps the channels to nx-ink, nx-paper, nx-white, nx-hairline, nx-slate-{300,500,700}, nx-accent{,-50,-100,-600,-700} and the two font families — all alpha-aware, all reading the same vars (so data-nx-product re-themes utilities too).


Dark theme

Dark is a first-class theme — one attribute re-themes every token, component, chart, and the mark.

  • Switch: set data-theme="dark" (or "light") on <html> — an app toggle or an SSR cookie. prefers-color-scheme: dark applies automatically unless data-theme="light" is set. NxThemeToggle flips the attribute.
  • How it works: the dark block overrides the neutral channel vars on :root; because the rgb() aliases derive from those channels, every component recolours with no per-component dark CSS. The mark’s n flips to light for free (it reads --nx-ink).
  • Accent tints are derived, not hand-picked: in dark, --nx-accent-50/100 become a color-mix() of the product accent into the dark surface, and --nx-accent-700 (text-on-tint) becomes a lightened accent — so every product, including any future one, dark-themes with zero extra values. --nx-accent (signature 600) stays vivid; --nx-on-accent stays white.
TokenLightDark
--nx-paper (app bg)#F7F8FA#0B0E14
--nx-white (surface)#FFFFFF#141922
--nx-surface-2#F1F3F7#1B212B
--nx-ink (text)#0B0E14#E7EAF0
--nx-slate-700#334155#C3C9D4
--nx-slate-500#5B6774#8B94A3
--nx-slate-300#CBD5E1#3A424E
--nx-hairline#E6E8EC#262D38

App surfaces are defined in both themes. The #0B1220 “on-dark” strip in the mark section is a fixed demo, independent of the app theme.

Status, chart & overlay tokens

Fixed, product-independent tokens (not remapped by data-nx-product):

  • On-accent: --nx-on-accent (#FFFFFF) — text/icons on an accent-600 fill (buttons, avatars, solid badges). Constant in both themes.
  • Status: --nx-success #0CA30C · --nx-warning #FAB219 · --nx-serious #EC835A · --nx-danger #D03B3B · --nx-info (= active accent). CVD-validated; always paired with an icon + label — never colour alone.
  • Overlays: --nx-scrim (dimmer) + --nx-shadow-overlay — the one sanctioned elevation, for drawers/modals/menus only (flat components stay flat).
  • Second surface: --nx-surface-2 — table headers, menus, drawers, inset panels.
  • Charts: --nx-chart-1…6 — a CVD-validated categorical palette (the dataviz reference instance; the product accents are too hue-close to serve as series), with its own dark steps, plus --nx-chart-grid / --nx-chart-axis.

3. The mark — nx ligature

The master device: a monoline n that flows into a crossing x. Path geometry is the single source of truth (shared with the SVG assets):

n: M92 380 L92 232 C92 140 178 128 244 156 L424 380
x: M424 156 L244 380
viewBox 0 0 512 512 · stroke-width 60 · round caps + joins
  • n = family constant, painted in --nx-mark-stroke (default --nx-ink, flips to paper on dark).
  • x = per-product accent (--nx-accent). The parent nx variant paints the x with a blue→violet→teal spectrum gradient.
  • Differentiation is accent + wordmark only — there is no per-product glyph.

3.1 NxMark (React) — props

PropTypeDefaultNotes
variant"nx" | "ord" | "check" | "point" | "cert" | "doc""nx"nx = neutral parent (spectrum x); others colour the x
animatedbooleanfalseDraw-on when scrolled into view; respects reduced-motion
tightbooleanfalseCrop viewBox to the glyph bbox (62 126 392 284) for inline/lockup use; default is the square canvas
titlestring"NX"Accessible label

The x colour resolves to var(--nx-accent, <variant signature>), so a standalone <NxMark variant="check" /> is blue even without a theme wrapper, while inside <NxThemeProvider> the provider’s accent wins.

3.2 Standalone SVG assets

Under assets/<member>/ (checknx · ordnx · pointnx · certnx · doclinq · nx):

FileWhatWhen
symbol.svg / favicon.svgAdaptive — n flips white in dark mode via prefers-color-scheme; accent x constantDefault favicon / inline symbol on light-or-auto surfaces
symbol-light.svgForced ink nKnown-light backgrounds
symbol-reversed.svgForced white nKnown-dark backgrounds (no media query)
symbol-mono.svgSingle currentColorInherits text colour; one-colour contexts
icon.svgRounded Ink tile + white n + accent xOS/app icon, avatar
lockup.svg / lockup-reversed.svgWordmark (prefix + accent NX), adaptive / darkHorizontal logo (not for parent nx)
png/…Raster 16/32/48/180/192/512/1024 (+-dark) + lockupsWhere SVG isn’t accepted

PNG status: exports exist for checknx/ordnx/pointnx; certnx, doclinq, nx, and all icon.svg are SVG-only until re-exported through a renderer (follow-up — no SVG→PNG tool currently available).

DocLinQ lockup is the family exception (§4). Its symbol*/icon/favicon are the standard token-driven mark in emerald; but its lockup.svg / lockup-reversed.svg render the full product word “DocLinQ” (all-ink) beside the ligature — not the generic <prefix>NX wordmark — because the product name doesn’t end in “NX”. This mirrors the app’s own Wordmark component (see §4).

Clear space & min size: keep at least the height of the n’s stem of clear space around the mark; symbol legible from 16px, wordmark from ~80px tall.


4. The lockup

Product labels render the prefix as ink text + the ligature standing in for “NX” — e.g. Check + mark.

<NxLockup product="check" size={20} /> // "Check" + blue nx mark
  • Prefix casing (as shipped): Check · ord · Point · Cert · (none for nx).
  • .nx-lockup__word — weight 780, tracking -0.02em, --nx-ink.
  • The mark is height: 1.05em, margin-left: .06em, NxMark tight.
  • size (px) sets the lockup font-size; the mark scales with it.
  • Accessible name is the full "<Prefix>NX"; the inline SVG is aria-hidden.

4.1 DocLinQ — the wordmark exception

DocLinQ is the one family member whose product name doesn’t end in “NX”, so the ligature can’t stand in for the trailing letters. Its lockup renders the full word “DocLinQ” (capital Q, all --nx-ink) followed by the ligature as a companion glyph — the emerald accent lives only in the mark’s x, not in the word. NxLockup product="doc" is therefore not used for DocLinQ (it would render PREFIX["doc"] = "doc" → “docNX”, react.tsx:99); the app ships a dedicated wordmark instead (doclinq/apps/main/src/components/wordmark.tsx), and assets/doclinq/lockup.svg mirrors it. The DOM order (word, then trailing mark) still matches the sibling .nx-lockup convention for suite cohesion. (Follow-up, kit-behaviour gate: if the family later wants a generic full-word lockup, extend NxLockup to accept a word override rather than the ${word}NX template.)


5. Components

All class names mirror the React components and require tokens.css + kit.css. Values below are the shipped defaults.

5.1 NxThemeProvider

Wraps a subtree, sets data-nx-product + .nx-app (applies --nx-font-sans, --nx-ink, font-smoothing). The single re-theming switch.

<NxThemeProvider product="ord"> … </NxThemeProvider>

5.2 NxButton.nx-btn

Base: font-size .875rem, weight 620, radius 10px, padding 9px 15px, 1px transparent border.

VariantRestHover
primary (.nx-btn--primary)bg --nx-accent, text whitebg --nx-accent-700
ghost (.nx-btn--ghost)transparent, text --nx-accent-700, border --nx-accent-100bg --nx-accent-50
outline (.nx-btn--outline, P0.5)transparent, text --nx-ink, border --nx-hairlinebg --nx-state-hover

--outline is the NEUTRAL secondary-action look (Cancel/Back/Retry/Download) — no accent tint, unlike --ghost. It’s a CSS-only class (apply directly via className="nx-btn nx-btn--outline", framework-agnostic like the rest of the kit); the bundled NxButton component’s variant union is unchanged (still "primary" | "ghost") since this pass didn’t touch react.tsx. Active/press (background:var(--nx-state-active)) lives in kit.css’s DS-1 component-states block, next to --ghost/--danger’s.

Focus: :focus-visible2.5px solid --nx-ink outline, 2px offset.

<NxButton>Start job</NxButton>
<NxButton variant="ghost">Assign</NxButton>

5.3 NxCard.nx-card / .nx-card__body

Surface: white, 1px --nx-hairline border, radius 18px, clipped. Body: padding 18px, vertical flex, gap 14px. The card is the standard container for a themed panel (usually NxProductHeader + body).

Sub-parts (P0.5, 2026-07-14 — DocLinq audit): .nx-card__header (padding 18px, vertical flex, gap 6px — consumers override flex-direction/alignment per instance, e.g. a KPI card’s row layout vs. a form card’s centered column) wraps .nx-card__title (1rem/700, tight leading, -0.01em tracking — same recipe as .nx-panel__title) and an optional .nx-card__description (.85rem, --nx-slate-500, normal leading). No .nx-card__footer — audited as unused across DocLinq’s real Card call sites, so YAGNI-cut; add it the same way if a future consumer needs it.

5.4 NxProductHeader.nx-header / .nx-header__avatar

Tinted header bar: bg --nx-accent-50, bottom border --nx-accent-100, padding 16px 18px, space-between. Holds an NxLockup and an optional avatar (26×26 circle, bg --nx-accent, white .7rem/700 initials).

<NxProductHeader product="check" avatar="AC" />

5.5 NxKpiTile.nx-kpi

Tinted stat tile: bg --nx-accent-50, radius 12px, padding 14px 16px. __num = 1.7rem/780, tracking -0.03em, color --nx-accent-700; __lbl = .78rem, --nx-slate-500.

<NxKpiTile value="14" label="Jobs today" />

5.6 NxListRow.nx-row

Row: flex, gap 12px, padding 10px 0, bottom 1px --nx-hairline divider. __icon = 30×30, radius 8px, bg --nx-accent-100, text --nx-accent-700, 700/.78rem. __title = .875rem/560, single-line ellipsis; __sub = .74rem, --nx-slate-500. Optional trailing NxStatusPill.

<NxListRow icon="NK" title="Nordküste — window clean" sub="09:30 · A. Berger" pill="On site" />

5.7 NxStatusPill.nx-pill

Chip: .68rem/640, padding 4px 9px, radius 999px, bg --nx-accent-100, text --nx-accent-700, no-wrap. For short states/counts.

5.8 NxField.nx-field

__label = .72rem/560, --nx-slate-500, 5px below-gap. __input = full-width, 1px --nx-hairline border, radius 10px, padding 9px 12px, .84rem, white bg; placeholder --nx-slate-500. Focus: border --nx-accent

  • 0 0 0 3px --nx-accent-100 glow (no default outline).
<NxField label="Search jobs" placeholder="Type to filter…" />

5.9 NxTabs.nx-tabs

Row of tabs: gap 18px, .82rem, --nx-slate-500, bottom 1px --nx-hairline, 10px bottom padding. Active (__item--active): color --nx-accent-700, weight 640, 2px accent underline flush to the divider. role="tablist" + per-tab role="tab" / aria-selected.

<NxTabs items={["Today", "Activity", "Settings"]} activeIndex={0} />

Extended components

All dark-aware and token-driven; class names mirror the React components in react.tsx. Framework-agnostic — the CSS classes work without React.

Form controls

NxCheckbox.nx-checkbox

Custom box, accent when checked, accent-100 focus glow, :has(input:disabled) dims. React: NxCheckbox.

NxRadio.nx-radio

Custom dot, accent when checked, accent-100 focus glow, :has(input:disabled) dims. React: NxRadio.

NxSwitch.nx-switch

38×22 track, accent when on, --nx-on-accent knob. React: NxSwitch.

NxSelect.nx-select

Styled native <select> with caret; focus = accent border + glow. React: NxSelect.

NxTextareatextarea.nx-field__input

The field input, min-height 88, vertical resize. React: NxTextarea.

Data

NxTable.nx-table

(+ .nx-table__wrap for a rounded bordered card) — uppercase muted th on --nx-surface-2, hairline row rules, tr:hover → accent-50, .num right-aligns with tabular figures. React: NxTable.

Status & feedback

NxBadge.nx-badge

Solid (default) + --soft / --success / --warning / --danger, optional __dot. React: NxBadge.

NxAlert.nx-alert

--info/--success/--warning/--danger — tinted surface, coloured left bar + icon, ink body text (never colour-alone). React: NxAlert.

NxProgress.nx-progress

__bar — accent bar on an accent-100 track. React: NxProgress.

NxSkeleton.nx-skeleton

Shimmer on --nx-surface-2, reduced-motion aware. React: NxSkeleton.

NxSpinner.nx-spinner

Small rotating ring, currentColor border-top, spin animation, reduced-motion aware — one of the primitives added in the DS-1 changelog. React: NxSpinner.

NxTooltip.nx-tooltip

__pop — CSS hover/focus tooltip; inverts per theme. React: NxTooltip.

Empty state — .nx-empty

Centered icon + title + copy + optional action for a list/table with no data yet — one of the primitives added in the DS-1 changelog. No dedicated NxKit wrapper — apply the class directly.

NxBreadcrumb.nx-breadcrumb

(+ __sep / __current). React: NxBreadcrumb.

NxMenu.nx-menu

__item (+ --active) / __sep — popover list with overlay shadow. React: NxMenu.

NxAppShell.nx-shell

Suite chrome (Phase 2 K1, 0.2.0): skip-link → sticky sidebar (__sidebar, __brand, __nav, __navlink/--active) → sticky top bar (__topbar, reuses .nx-header’s accent-tinted look) with __topbar-start (hamburger + NxAppSwitcher) / __topbar-center (search slot) / __topbar-end (themeToggle/localeSwitcher/user slots) → #main-content (__content). Below 768px the sidebar hides and the hamburger opens the same nav via NxDrawer (reused, not reimplemented). Framework-agnostic — linkComponent prop, no next/* import. React: NxAppShell. See ADOPTION.md for the full prop table.

NxAppSwitcher.nx-appswitcher

Top-bar product menu (Phase 2 K1, 0.2.0): icon trigger (__trigger, aria-haspopup="menu") opens a 2-col grid __panel of __tile/--current product tiles (NxMark + label, role="menuitem"). Escape/outside-click close; focus-trap in and back out via the kit’s useFocusTrap. Renders every apps entry it’s given — entitlement filtering is the consumer’s job (Phase 3), not this component’s. React: NxAppSwitcher.

Overlays

Both below use --nx-scrim + --nx-shadow-overlay.

NxDrawer.nx-drawer

(+ --left), composed from the shared .nx-panel__head / __body / __foot. React: NxDrawer.

NxModal.nx-modal

__card, composed from the shared .nx-panel__head / __body / __foot. React: NxModal.

Primitives

NxDivider.nx-divider

1px hairline rule for separating stacked content within a panel. React: NxDivider.

NxAvatar.nx-avatar

Circular accent-filled initials badge. React: NxAvatar.

Icon button — .nx-iconbtn

Square icon-only control for compact actions (close, more, toggle) — no dedicated NxKit wrapper, apply the class directly.

Other primitives in this group: .nx-btn--danger, .nx-btn--sm, .nx-btn--outline (P0.5) (Button modifiers — see NxButton). Theme control: NxThemeToggle (flips data-theme on <html>).


Charts

Charts follow the data-viz method — form → colour-by-job → validate → marks → hover → a11y — built as token-driven SVG.

  • Categorical palette: --nx-chart-1…6, the CVD-validated dataviz reference set (worst adjacent ΔE ≥ 12 in light; the floor band in dark, so ≥ 4 series lean on direct labels / legend). Assign in fixed order; a 7th series folds into “Other” or small multiples — never a generated hue.
  • Marks: thin (2px lines, ≥ 8px markers, 4px rounded bar data-ends), a 2px surface gap between adjacent/stacked fills, recessive grid (--nx-chart-grid) and axis (--nx-chart-axis), selective direct labels (never a number on every point), a legend whenever ≥ 2 series.
  • Text wears ink/slate, never the series colour. Status colours are reserved (never “series 4”). One y-axis only — never dual-axis.
  • Dark: the palette ships its own validated dark steps (auto via data-theme). Chrome classes: .nx-chart, .nx-grid line, .nx-axis, .nx-legend.
  • Magnitude/polarity: sequential = one-hue light→dark ramp; diverging = two poles + a neutral gray midpoint (see the dataviz skill for the ramps).

The showcase renders bar (single series), line + area (two series), a donut, and stat tiles against these tokens.


6. Accessibility

  • Focus: interactive controls show a visible focus indicator — buttons use a 2.5px ink outline (:focus-visible); inputs use the accent border + glow. Never remove focus styling without an equivalent.
  • Contrast: use accent-700 for text on accent-50/100 tints; accent-600 is for fills and large/graphic elements. Body text is --nx-ink on white/paper.
  • Motion: all mark animation is gated behind prefers-reduced-motion.
  • Semantics: tabs expose role="tablist"/"tab" + aria-selected; the lockup carries the full accessible name while its SVG is aria-hidden.
  • Icon-only content (row icons, avatars) is decorative shorthand — pair with a text title/sub for meaning.

7. Do / Don’t

Do

  • Theme with one data-nx-product / NxThemeProvider; let tokens cascade.
  • Use accent tints (50/100) for surfaces and 600/700 for ink-on-tint + actions.
  • Reach for NxMark tight inside text (lockups); square elsewhere.
  • Keep the mark’s n neutral and let only the x carry colour.

Don’t

  • Hard-code hex in components — always go through --nx-*.
  • Add drop shadows or a second accent to a single product surface.
  • Recolour the mark’s n, add a per-product glyph, or outline the wordmark.
  • Build certNX’s certificate/portal surface from this kit (see §8).
  • Merge the DRAFT staging PRs (checknx#78, ordnx#28) — they carry the superseded June assets; regenerate from assets/ first.

8. certNX is standalone

certNX belongs to the family by mark and tokens — use <NxMark variant="cert" />, the azure ramp, and assets/certnx/ for badges/seals. But the shared app kit (NxProductHeader, NxKpiTile, list rows, everyday-app surface) is the core-three kit. Certification carries disclosure/trust obligations and gets a more formal, document-grade surface, so certNX credentials and audit views should not be built from these components. Same family, deliberate distance.

Docs-site decision (DS-3): certNX is the family’s one non-family-app, standalone azure mark — it gets no dedicated component/foundation entry and no standalone certNX docs page in docs-site/; the caveat above (and the accent-ramp row in §1.2) is the complete, intentional extent of its coverage here.

(Open partner call: reconcile this family certNX mark vs the older ../../templates/brand/assets/certnx.svg — which is canonical? See company/tasks.md.)


9. Where things live

brand/nx-family/
├── design.md ← this file (the spec)
├── README.md ← install + usage guide
├── FAMILY-PREVIEW.html ← canonical family preview
├── index.html ← living gallery: every brand + component (+ theme playground)
├── demo.html ← kit verification page
├── tokens.css ← foundations (source of truth for §1–§2)
├── kit.css ← component styles (source of truth for §5)
├── tailwind.nx.cjs ← optional Tailwind token map
├── mark.tsx ← the mark (§3)
├── react.tsx ← components (§4–§5)
└── assets/<member>/ ← standalone SVG (+ PNG) exports (§3.2)

DS-1 changelog

Foundations pass on top of §1–§9 above: scale tokens, interaction states, a density mode, a11y hooks/behaviors, an RTL logical-property sweep, and a dev-only axe self-audit in the showcase. Full reference tables (values, usage matrices) are deferred to DS-2 — this is a pointer to the new names plus the breaking list below.

New tokens (pointer only — see tokens.css for values):

  • Scale: --nx-space-*, --nx-size-*, --nx-radius-*, --nx-border*
  • Focus/ring: --nx-focus-outline-*, --nx-ring, --nx-ring-danger
  • Motion: --nx-dur-*, --nx-ease-*
  • Layering: --nx-z-* (incl. --nx-z-root, see breaking change 2 below)
  • Type: --nx-fw-*, --nx-leading-*
  • Layout: --nx-row-icon
  • Status: new dark overrides, plus --nx-success-solid, --nx-danger-50, --nx-danger-100
  • State: --nx-disabled-opacity, --nx-state-hover, --nx-state-active, --nx-press-y, --nx-selected-bg

New primitives & mechanisms:

  • Density: data-nx-density attribute + density-scoped role tokens (compact/comfortable).
  • Primitives: .nx-empty, .nx-spinner, .nx-sr-only.
  • A11y behaviors: useFocusTrap, useBodyScrollLock, roving-tabindex menu/tabs, live-region alerts, a hoverable + dismissible tooltip, and forced-colors + reduced-motion coverage.
  • RTL: logical-property sweep across the kit.
  • Showcase: a dev-only axe self-audit (see breaking change 4 for known, by-design findings).

Breaking changes (adopt-time)

Flag for checknx / ordnx / pointnx / cosmethicals maintainers before bumping:

  1. NxTabs markup changed <span><button role="tab">. Any consumer CSS/markup keyed on .nx-tabs span must move to the class/role selector. Roving tabindex means the tablist is now one tab stop (was N stops). aria-controls is now opt-in via a new panels prop (default off) — pass panels and render matching role="tabpanel" elements. Id contract: tab ${idBase}-${i} ↔ panel ${idBase}-panel-${i}.
  2. Z-index band moved from ~100 to a documented 1000–1600 band. Host chrome (toasts, banners) still living in the old 100–999 range now renders below kit overlays — mitigate with --nx-z-root to offset the whole band.
  3. .nx-drawer--left now means inline-start (auto-flips under RTL). The API name is unchanged; the semantic is no longer “always screen-left.”
  4. Contrast nudges are deferred (Task 10, partner-gated). The AA color-contrast fixes for --nx-slate-500, pointNX’s --nx-accent-700, and white-on-light-accent tiles are not part of DS-1 — applying them repaints live products. Until they land, the showcase’s axe self-audit reports color-contrast violations for these by design; don’t file them as DS-1 regressions.

Notes:

  • NxAlert live-region caveat: a polite (role="status") alert announces reliably only if the live region already exists in the DOM before its text changes. Render a persistent, empty alert container and inject text into it (mount-empty-then-populate) — don’t mount the alert already carrying its final text.
  • Spec reconciliation: the compact --nx-menu-item-min-h ships at 28px (= --nx-size-7); the DS-1 planning spec’s table said 30px. Both are ≥24px WCAG-safe — 28px is canonical, the planning doc is superseded here.

DS-added: doc (DocLinq/emerald) family member.

P0.5 — DocLinq re-skin primitive gap-fill (2026-07-14)

Task P0.5 audited DocLinq’s actual HeroUI component usage (apps/main/src, apps/website) ahead of its Phase 2 nx-* re-skin, to close only the primitive gaps real call sites need (YAGNI — no speculative variants). CSS-only; react.tsx/ mark.tsx untouched.

Added:

  • .nx-btn--outline (+ :hover/:active) — HeroUI variant="bordered" (dominant labeled-secondary-button variant in DocLinq, 24 call sites) and variant="flat" (3 call sites, all semantically-equivalent dialog Cancel buttons — consolidated onto the same class rather than adding a fourth near-duplicate neutral modifier). Neutral tone (--nx-ink text, --nx-hairline border, --nx-state-hover/ -active overlay) — deliberately distinct from the accent-tinted --ghost, matching the “NEUTRAL controls use --nx-state-*” rule already documented for the DS-1 state model.
  • .nx-card__header / __title / __description — DocLinq’s CardHeader always wraps CardTitle(+CardDescription); ~19 real call sites across dashboard tiles and auth/settings forms.

Audited, found already covered (no change): icon-only buttons (real usage is variant="ghost"/"bordered" + isIconOnly or icon-only sizing — maps onto the existing .nx-iconbtn, per the brief’s stated preference); the [aria-busy] + .nx-btn__label + .nx-spinner loading contract (DocLinq’s 4 auth submit buttons already wire aria-busy by hand — the existing contract is an exact match); Badge and Alert (each has exactly one real call site, neither passes a color/severity prop — the existing --soft/--success/--warning/--danger sets already exceed what’s used); Input/Textarea/NumberInput/Select field states (label, description, isRequired, isInvalid, errorMessage) — already fully carried by .nx-field__label/__hint/__error + [aria-invalid]; Tabs/Checkbox/Radio (default variant only); Progress/Switch/the design-system Tooltip (not used anywhere in DocLinq today).

Audited, found NOT needed despite being called “likely missing” in the task brief (i.e. the brief’s a-priori guess didn’t survive contact with real usage — flagging so the next audit doesn’t re-add these speculatively):

  • .nx-btn--md/--lg — DocLinq never passes an explicit Button size other than "sm" (already covered by the existing .nx-btn--sm).
  • .nx-btn--success/--warning — DocLinq’s only Button color values are "primary" and "danger" (and the untinted default), both already covered.
  • Avatar size modifiers — DocLinq’s one real Avatar usage renders at exactly 32px (h-8 w-8), i.e. the existing default .nx-avatar (--nx-avatar-md). No second size is used, so none was added.

Considered, deliberately deferred (not a kit.css concern):

  • Button leading/trailing icons (startContent/endContent, ~4 real call sites) and full-width text-centering (submit buttons, 4 call sites) both need display:inline-flex/gap/justify-content — properties .nx-btn doesn’t set today. Cross-repo precedent (PointNX’s Hero.tsx: className="nx-btn nx-btn--primary inline-flex h-12 min-h-11 gap-2 px-6 ...") shows the established convention is consumer-side Tailwind layout utilities alongside the kit’s color/shape classes, not baking layout into the shared .nx-btn rule — so DocLinq’s re-skinned Button wrapper should add its own inline-flex items-center justify-center gap-* base classes, not kit.css.
  • One-off shape overrides reached via className rather than a component prop — the nav-user Avatar’s rounded-lg (square, not circular) and a handful of Skeleton shapes (rounded-full/rounded-none) — are per-instance styling the same way arbitrary width/height already is, not a prop-level variant. Note for the re-skin: DocLinq’s global CSS loads kit.css unlayered (apps/main/src/app/[locale]/layout.tsx imports it before @workspace/ui/globals.css’s @import "tailwindcss"), and per the CSS Cascade Layers spec unlayered rules beat any @layer-wrapped rule (Tailwind v4 utilities included) regardless of source order — so a Tailwind shape utility alongside an nx-* class that already sets that same property will silently lose. The reliable override for a one-off is an inline style={{ borderRadius: "var(--nx-radius-full)" }} (still 100% token-driven), not a new shared modifier for a single call site.
  • A leading-icon affordance for .nx-field__input (one real call site: the search page’s search icon + clear button) — needs a wrapper/slot structure with no existing kit precedent to match; flagged for whoever re-skins that screen rather than invented here.
  • Select selectionMode="multiple" (approver/acknowledger pickers) — HeroUI’s default multi-select renderValue is plain comma-joined text (neither call site supplies a custom renderValue), so the existing single-line .nx-select styling already covers it visually; the multi-select behavior itself is a react.tsx / ARIA-listbox concern, out of scope for this CSS-only pass.

Status: review — the mark + palettes are partner-approved (2026-07-03); adopting this kit as the official in-app identity across the products is an open partner gate (company/tasks.md, [ORG-14]).