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 singledata-nx-productattribute re-themes an entire subtree. - Alpha-aware. Colors are stored as
R G Bchannels, so any token composes with an opacity (rgb(var(--nx-accent-600-rgb) / .7)or Tailwindbg-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)
| Token | Hex | R G B | Role |
|---|---|---|---|
--nx-ink | #0B0E14 | 11 14 20 | Primary text, the mark’s n, focus ring |
--nx-slate-700 | #334155 | 51 65 85 | Strong secondary text |
--nx-slate-500 | #5B6774 | 91 103 116 | Muted text, labels, placeholders |
--nx-slate-300 | #CBD5E1 | 203 213 225 | Disabled / faint |
--nx-hairline | #E6E8EC | 230 232 236 | Borders, dividers |
--nx-paper | #F7F8FA | 247 248 250 | App background; the mark’s n on dark |
--nx-white | #FFFFFF | 255 255 255 | Card / 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):
| Step | Alias | Used for |
|---|---|---|
| 50 | --nx-accent-50 | Section/tile fills (header, KPI), ghost-button hover |
| 100 | --nx-accent-100 | Chips/pills bg, row-icon bg, focus-ring glow, hairlines on tinted areas |
| 600 | --nx-accent / --nx-accent-600 | Primary buttons, the mark’s x, active underline, links |
| 700 | --nx-accent-700 | Text 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
| Token | Stack |
|---|---|
--nx-font-sans | ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif |
--nx-font-mono | ui-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):
| Name | Size | Weight | Tracking | Where |
|---|---|---|---|---|
| KPI number | 1.7rem | 780 | -0.03em | .nx-kpi__num |
| Lockup word | 1em (inherits) | 780 | -0.02em | .nx-lockup__word |
| Body / control | 0.875rem | 620 | — | buttons, row titles |
| Input text | 0.84rem | 400 | — | .nx-field__input |
| Tab | 0.82rem | 640 (active) | — | .nx-tabs |
| KPI label / sub | 0.78rem / 0.74rem | 400 | — | labels, list subtext |
| Field label | 0.72rem | 560 | — | .nx-field__label |
| Avatar / pill | 0.70rem / 0.68rem | 700 / 640 | — | avatar, .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 use112on the 512 canvas (≈ 22%). - Padding rhythm: card body
18px· header16px 18px· KPI14px 16px· input9px 12px· button9px 15px. Vertical stack gap inside a card body is14px; tab gap18px.
1.5 Motion
Superseded by DS-1 — see the note at the top of §1.4.
| Token | Value | Use |
|---|---|---|
--nx-ease | cubic-bezier(.22,.68,.16,1) | All kit transitions + the mark draw-on |
- Mark draw-on (
.nx-mark.is-playing): thenstrokes in over0.6s, then the accentxfollows (0.5sdelay). 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:
- 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>. rgb()aliases (--nx-accent-600,--nx-accent, …) — convenience, derived from the channels. Declared on both:rootand 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: darkapplies automatically unlessdata-theme="light"is set.NxThemeToggleflips the attribute. - How it works: the dark block overrides the neutral channel vars on
:root; because thergb()aliases derive from those channels, every component recolours with no per-component dark CSS. The mark’snflips to light for free (it reads--nx-ink). - Accent tints are derived, not hand-picked: in dark,
--nx-accent-50/100become acolor-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-accentstays white.
| Token | Light | Dark |
|---|---|---|
--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 380x: M424 156 L244 380 viewBox 0 0 512 512 · stroke-width 60 · round caps + joinsn= family constant, painted in--nx-mark-stroke(default--nx-ink, flips to paper on dark).x= per-product accent (--nx-accent). The parentnxvariant paints thexwith a blue→violet→teal spectrum gradient.- Differentiation is accent + wordmark only — there is no per-product glyph.
3.1 NxMark (React) — props
| Prop | Type | Default | Notes |
|---|---|---|---|
variant | "nx" | "ord" | "check" | "point" | "cert" | "doc" | "nx" | nx = neutral parent (spectrum x); others colour the x |
animated | boolean | false | Draw-on when scrolled into view; respects reduced-motion |
tight | boolean | false | Crop viewBox to the glyph bbox (62 126 392 284) for inline/lockup use; default is the square canvas |
title | string | "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):
| File | What | When |
|---|---|---|
symbol.svg / favicon.svg | Adaptive — n flips white in dark mode via prefers-color-scheme; accent x constant | Default favicon / inline symbol on light-or-auto surfaces |
symbol-light.svg | Forced ink n | Known-light backgrounds |
symbol-reversed.svg | Forced white n | Known-dark backgrounds (no media query) |
symbol-mono.svg | Single currentColor | Inherits text colour; one-colour contexts |
icon.svg | Rounded Ink tile + white n + accent x | OS/app icon, avatar |
lockup.svg / lockup-reversed.svg | Wordmark (prefix + accent NX), adaptive / dark | Horizontal logo (not for parent nx) |
png/… | Raster 16/32/48/180/192/512/1024 (+-dark) + lockups | Where SVG isn’t accepted |
PNG status: exports exist for
checknx/ordnx/pointnx;certnx,doclinq,nx, and allicon.svgare 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/faviconare the standard token-driven mark in emerald; but itslockup.svg/lockup-reversed.svgrender the full product word “DocLinQ” (all-ink) beside the ligature — not the generic<prefix>NXwordmark — because the product name doesn’t end in “NX”. This mirrors the app’s ownWordmarkcomponent (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 fornx). .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 lockupfont-size; the mark scales with it.- Accessible name is the full
"<Prefix>NX"; the inline SVG isaria-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.
| Variant | Rest | Hover |
|---|---|---|
primary (.nx-btn--primary) | bg --nx-accent, text white | bg --nx-accent-700 |
ghost (.nx-btn--ghost) | transparent, text --nx-accent-700, border --nx-accent-100 | bg --nx-accent-50 |
outline (.nx-btn--outline, P0.5) | transparent, text --nx-ink, border --nx-hairline | bg --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-visible → 2.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-100glow (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.
NxTextarea — textarea.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.
Navigation
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
datavizskill 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.5pxink 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-inkon 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 isaria-hidden. - Icon-only content (row icons, avatars) is decorative shorthand — pair with a
text
title/subfor 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 tightinside text (lockups); square elsewhere. - Keep the mark’s
nneutral and let only thexcarry 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-densityattribute + 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, andforced-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:
NxTabsmarkup changed<span>→<button role="tab">. Any consumer CSS/markup keyed on.nx-tabs spanmust move to the class/role selector. Roving tabindex means the tablist is now one tab stop (was N stops).aria-controlsis now opt-in via a newpanelsprop (default off) — passpanelsand render matchingrole="tabpanel"elements. Id contract: tab${idBase}-${i}↔ panel${idBase}-panel-${i}.- 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-rootto offset the whole band. .nx-drawer--leftnow means inline-start (auto-flips under RTL). The API name is unchanged; the semantic is no longer “always screen-left.”- Contrast nudges are deferred (Task 10, partner-gated). The AA
color-contrastfixes 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 reportscolor-contrastviolations for these by design; don’t file them as DS-1 regressions.
Notes:
NxAlertlive-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-hships 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) — HeroUIvariant="bordered"(dominant labeled-secondary-button variant in DocLinq, 24 call sites) andvariant="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-inktext,--nx-hairlineborder,--nx-state-hover/-activeoverlay) — 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’sCardHeaderalways wrapsCardTitle(+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 Buttonsizeother than"sm"(already covered by the existing.nx-btn--sm)..nx-btn--success/--warning— DocLinq’s only Buttoncolorvalues 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 needdisplay:inline-flex/gap/justify-content— properties.nx-btndoesn’t set today. Cross-repo precedent (PointNX’sHero.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-btnrule — so DocLinq’s re-skinned Button wrapper should add its owninline-flex items-center justify-center gap-*base classes, not kit.css. - One-off shape overrides reached via
classNamerather than a component prop — the nav-user Avatar’srounded-lg(square, not circular) and a handful ofSkeletonshapes (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 loadskit.cssunlayered (apps/main/src/app/[locale]/layout.tsximports 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 annx-*class that already sets that same property will silently lose. The reliable override for a one-off is an inlinestyle={{ 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-selectrenderValueis plain comma-joined text (neither call site supplies a customrenderValue), so the existing single-line.nx-selectstyling already covers it visually; the multi-select behavior itself is areact.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]).