@northwestu/nuxt-nu-design-kit
v0.1.0
Published
Northwest University marketing design system as a Nuxt 4 layer — tokens, CSS components, and Vue wrappers.
Maintainers
Readme
@northwestu/nuxt-nu-design-kit
Northwest University marketing design system as a Nuxt 4 layer.
Packages the official stylesheet from northwestu.edu (assets/stylesheets/style-2026.css) with thin, auto-imported Vue wrappers (NButton, NCard, NSection, …) for ergonomic use in Nuxt apps. Component demos in the playground mirror the design-system examples.
Features
- Design tokens — brand colors, space, type, radii, shadows, breakpoints (
--nu-*CSS custom properties) - Official 2026 CSS — linked externally from
www.northwestu.edu/assets/stylesheets/style-2026.cssat runtime, so consumers always get the current upstream styles; a pinned vendored copy (nu-design-system.css) ships for offline/self-hosted use - Compatibility layer —
nu-compat.cssrestores legacy classes the 2026 sheet no longer ships (stack/layout primitives,section-*tones,prose, card sub-parts, …) by aliasing them onto the 2026--nu-*tokens - 40 Vue wrappers — auto-imported
N*components over the design-system classes - Interactions — fade-in on scroll (plugin + composable), reel/carousel controls
- Typekit fonts — Proxima Nova + Zenon via the NU kit stylesheet (optional)
- Tested — Vitest unit + a11y contracts, Playwright e2e + axe scans
Requirements
- Nuxt
^3.13.0 || ^4.0.0 - Vue
^3.5.0
Installation
The package is published to npm as @northwestu/nuxt-nu-design-kit. No authentication needed.
1. Install the layer
npm install @northwestu/nuxt-nu-design-kit
# or
pnpm add @northwestu/nuxt-nu-design-kit2. Extend Nuxt
// nuxt.config.ts
export default defineNuxtConfig({
extends: ['@northwestu/nuxt-nu-design-kit'],
})Extending the layer gives you:
- The full design-system stylesheet (external 2026 sheet via runtime
<link>+ bundledapp/assets/css/main.css→nu-compat.css) - All
N*components, auto-imported useNFadeIn/useNReelcomposables and thenClassesutil, auto-imported- A client plugin that activates
.fade-in/.fade-in-childrenscroll reveals on every page and after each route change html[lang="en"]and runtime-controlled<head>links for the official stylesheet and Adobe Fonts (Typekit) — toggleable via runtimeConfig
Configuration
The layer's head resources are injected at runtime by a plugin, so consumers can toggle them per environment — in nuxt.config or via NUXT_PUBLIC_* env vars at deploy time.
Stylesheet
By default the official 2026 stylesheet is linked externally (https://www.northwestu.edu/assets/stylesheets/style-2026.css). To self-host instead (CSP, offline builds):
// nuxt.config.ts (consumer)
export default defineNuxtConfig({
extends: ['@northwestu/nuxt-nu-design-kit'],
runtimeConfig: {
public: {
nDesignKit: { stylesheet: 'none' },
},
},
})Then load the pinned vendored copy yourself:
css: ['@northwestu/nuxt-nu-design-kit/css/nu-design-system.css'],Or set NUXT_PUBLIC_N_DESIGN_KIT_STYLESHEET=none at runtime.
Typekit / fonts
The layer injects the NU Typekit stylesheet (https://use.typekit.net/lqz8krm.css) into <head>. If you self-host fonts:
runtimeConfig: {
public: {
nDesignKit: { typekit: false },
},
},
// or NUXT_PUBLIC_N_DESIGN_KIT_TYPEKIT=falseApp config
The layer exposes typed app config for debugging/introspection:
// app.config.ts (consumer)
export default defineAppConfig({
nDesignKit: {
name: '@northwestu/nuxt-nu-design-kit',
version: '0.1.0',
},
})Usage
<template>
<NSection tone="midnight">
<NContainer>
<h1>Welcome to Northwest University</h1>
<NHStack>
<NButton variant="primary" to="/apply">Apply</NButton>
<NButton variant="secondary" href="/visit">Visit</NButton>
</NHStack>
</NContainer>
</NSection>
</template>CSS-only
You can also use design-system class names directly:
<a class="button primary" href="/apply">Apply</a>
<div class="card">…</div>Or import the stylesheet without extending the full layer:
// nuxt.config.ts
export default defineNuxtConfig({
css: ['@northwestu/nuxt-nu-design-kit/css'],
})Package exports:
| Export | Resolves to |
|--------|-------------|
| @northwestu/nuxt-nu-design-kit | ./nuxt.config.ts (layer entry) |
| @northwestu/nuxt-nu-design-kit/css | ./app/assets/css/main.css |
| @northwestu/nuxt-nu-design-kit/css/* | ./app/assets/css/* |
Design tokens
Tokens are CSS custom properties on :root:
.hero {
background: var(--nu-navy);
color: var(--nu-color-white);
padding: var(--nu-space-8);
border-radius: var(--nu-border-radius-6);
}| Token | Value | Role |
|-------|-------|------|
| --nu-blue | #0068bb | Primary brand |
| --nu-navy | #034C87 | Deep brand |
| --nu-alt | #148dcd | Alt blue |
| --nu-amber | #ffbc2d | Secondary CTA |
| --nu-evergreen | #04505c | Supporting green |
| --nu-powder | #eaf4fb | Soft surface |
See the token block at the top of app/assets/css/nu-design-system.css for the full list (spacing scale --nu-space-*, radii --nu-border-radius-*, size scale --nu-size-*, typography, breakpoints).
The stylesheet also ships utility classes used across the demos, e.g. spacing utilities (mb-8, p-10) and the .inset full-cover link pattern; sizing (size-1…size-10) and aspect-ratio-* utilities are provided by the compat layer.
Compatibility layer
app/assets/css/nu-compat.css (imported after the official sheet) restores the legacy classes the 2026 stylesheet no longer ships — hstack/vstack/zstack/cover/cluster, section-* tones, prose, avatar, card-header/card-content/card-footer, blog, contact, cta, directory, feature, code-example, calendar-container/calendar-header, divider, size-*, aspect-ratio-*, scroll-reveal, and related modifiers. Its rules were carried over from the previous design-system stylesheet and resolve through aliases onto the 2026 --nu-* tokens, so tone/value changes upstream still flow through. Unlike the vendored file, nu-compat.css is maintained in this repo.
Component reference
All components are auto-imported. Every component accepts a class?: string prop that is merged onto its root element, and attributes fall through to the root. Wrappers emit the markup/classes expected by the 2026 stylesheet (plus compat-layer classes where noted above).
Actions & navigation
| Component | Root / classes | Props | Slots |
|-----------|----------------|-------|-------|
| NButton | .button (+ primary / secondary / contrast) on NuxtLink, <a>, or <button> | variant, to, href, external (adds target="_blank" rel="noopener noreferrer"), type ('button'), disabled | default |
| NBreadcrumbs | .breadcrumbs > nav[aria-label="Breadcrumb"] | items: { label, to?, href? }[] (required) — first item renders the home icon, the current item renders <span aria-current="page"> | — |
| NSubnav | nav.subnav > .container > .department-nav > ul (2026 markup; compat keeps the list visible on mobile) | items: { label, to?, href?, current?, external? }[] | default (custom items) |
Content & marketing blocks
| Component | Root / classes | Props | Slots |
|-----------|----------------|-------|-------|
| NBanner | .banner > .container | title | title, default, action |
| NBlog | .blog on NuxtLink / <a> / <article>; inner .hstack > img + .vstack.gap-0 | to, href, title, meta, image, imageAlt | title, meta, default |
| NCard | article.card | aspectRatio?: 2 \| 4 | default |
| NCardHeader | img.card-header (with src) or .card-header element | as, src, alt | default |
| NCardContent | .card-content | — | default |
| NCardFooter | footer.card-footer | — | default |
| NContact | .contact | name, title, email, phone, avatar, avatarAlt — email/phone render as mailto:/tel: links | name, title, email, phone, default |
| NCta | .cta > .prose | title, description | title, description, default |
| NDirectory | .directory, columns in .hstack.top | title | title, default |
| NFeature | .feature — full-width h2, hr.divider, .layout > .main + .aside | title, tag | title, tag, aside, default |
| NListing | ul.listing, items get a full-cover .inset link labelled by the item title | items: { title, href?, to?, description? }[] | item-${i} (scoped { item }), default |
| NQuote | figure.quote > blockquote + figcaption | cite, author, scrollReveal | default, caption |
| NStats | .stats (+ accent-1…accent-4) | accent, title (convenience; official markup nests NStat inside a heading via the default slot) | title, default |
| NStat | span.stat | value?: string \| number | default |
Layout primitives
| Component | Root / classes | Props | Slots |
|-----------|----------------|-------|-------|
| NSection | <section> (+ section-<tone>) | tone: default, blue, alt, sky, skylight, powder, navy, midnight, evergreen, leaf, gold, sun, cloud, wisp, white; as (default 'section') | default |
| NContainer | .container or .container-1….container-10 | size?: 0–10 \| 'default', as | default |
| NHStack | .hstack | reverse, top, between, stretch, noWrap (bool modifiers), as | default |
| NVStack | .vstack | as | default |
| NZStack | .zstack | as | default |
| NCenter | .center | as | default |
| NCover | .cover | as | default |
| NCluster | .cluster | as | default |
| NSticky | .sticky | as | default |
| NDivider | hr.divider | — | — |
| NSpacer | .spacer (aria-hidden) | — | — |
UI & data
| Component | Root / classes | Props | Slots |
|-----------|----------------|-------|-------|
| NAccordion | .accordion, native <details>/<summary> | items: { title, content?, open? }[] | item-${i} (scoped { item }), default |
| NAccordionItem | <details> (inside NAccordion default slot) | title (required), open | default |
| NAvatar | img.avatar (+ size-1…size-5; no class = default 72px) | src (required), alt, size?: 1–5 | — |
| NBadge | span.badge (+ variant) | variant: default, info, danger, warning, success, highlight | default |
| NCalendar | .calendar-container + .calendar-header (compat) wrapping a table.cal month grid (2026) | title, subtitle | header, default, table |
| NCode | .code-example | padded (default true; false adds p-0) | default |
| NTable | .table > table | caption | caption, default |
| NTooltip | data-tooltip attributes, pure CSS | text (required), position?: 'top' \| 'bottom', as | default (trigger) |
| NTypography | .prose | as | default |
Motion & carousel
| Component | Root / classes | Props | Slots |
|-----------|----------------|-------|-------|
| NFadeIn | .fade-in or .fade-in-children | children?: boolean, as | default |
| NReel | .reel[role="region" tabindex="0"] > .reel-container > ul.reel-list, controls in .reel-navigation | showControls (default true) | default (reel items) |
| NReelItem | li.reel-item + reel-content-light / reel-content-dark | image, imageAlt, contentTone?: 'light' \| 'dark' | default |
NFadeIn runs its own IntersectionObserver (threshold 0.1, root margin 0px 0px -100px 0px) and adds .visible once when scrolled into view. The layer's client plugin does the same for plain .fade-in / .fade-in-children markup on every page and after each route navigation.
Composables
// Scoped fade-in observer (e.g. for dynamically rendered content)
const { observe } = useNFadeIn(rootElement?) // observe() re-scans
// Reel scroll state + controls (used internally by NReel)
const { canPrev, canNext, prev, next, update } = useNReel(scrollerElement)prev() / next() smooth-scroll by one .reel-item width + gap; canPrev / canNext track scroll position and drive the disabled state of the controls.
Class-merge util
nClasses('button', variant && variant, { 'no-wrap': true }, props.class)
// clsx-style: strings passthrough, object keys when truthy, falsy skippedKnown differences from the official docs
NSubnavrenders a flat item list; the official<details>/<summary>dropdown submenus and mobile.department-nav-buttontoggle are not implemented (compat CSS keeps the list visible below 960px instead).NCalendaris a shell: it renders the 2026table.calgrid container, but event/day interactions (.day-link,.event-link) are markup you supply via thetableslot.NAvatarrenders<img>only; the officialsvg.avataricon variant is not implemented.NStats'stitleprop is a convenience not present in the official markup — prefer the default slot with the official nested-heading pattern (<h4><NStat>…</NStat> label</h4>).
Development
npm install --legacy-peer-deps
npx playwright install chromium # first time only (e2e / a11y)
npm run dev # docs-style playground at http://localhost:3000
npm run build # build playground
npm run generate # static-generate playground
npm run lintThe layer lives at the repo root. .playground/ extends .. so you can iterate on components against a real Nuxt app; every component has a demo page at /components/<slug> that mirrors the official docs page and links to it.
| Script | What it does |
|--------|--------------|
| dev | nuxi dev .playground — playground with hot reload |
| build / generate / preview | Build / static-generate / preview the playground |
| lint | ESLint over the repo |
| test / test:watch / test:coverage | Vitest unit + a11y contracts |
| test:e2e / test:e2e:ui | Playwright smoke + axe scans |
| test:a11y | axe-core suite only |
| test:all | unit + e2e/a11y |
Testing
| Suite | Tooling | What it covers |
|-------|---------|----------------|
| Unit | Vitest, @nuxt/test-utils, happy-dom | Key N* components, composables, a11y contracts |
| E2E | Playwright | Playground load, all 31 component anchors, smoke interactions |
| A11y | Playwright + @axe-core/playwright | WCAG 2.1 A/AA serious/critical violations, landmarks, alt text, focus |
E2E uses port 3333 by default so it does not attach to an unrelated app on :3000. Override with E2E_PORT=…. Set E2E_REUSE=1 only if you intentionally want to reuse a running server.
Sync upstream CSS
By default the layer links https://www.northwestu.edu/assets/stylesheets/style-2026.css externally at runtime (see Configuration → Stylesheet). A pinned copy is also vendored at app/assets/css/nu-design-system.css for reference and for consumers who must self-host (CSP, offline builds). To refresh the pinned copy:
scripts/sync-css.sh
# or a specific URL:
scripts/sync-css.sh "https://www.northwestu.edu/assets/stylesheets/style-2027.css"The script re-downloads the stylesheet and re-adds the attribution header. Do not hand-edit the vendored file without re-syncing upstream — kit-specific fixes belong in app/assets/css/nu-compat.css.
Publishing (npm)
The package publishes to the public npm registry as @northwestu/nuxt-nu-design-kit. prepublishOnly runs nuxt prepare .playground as a final sanity check.
Publishing is automated: pushing a GitHub Release runs .github/workflows/publish.yml, which installs, runs unit tests, and publishes with provenance. One-time setup: create a granular access token at npmjs.com with read/write access to the northwestu org's packages, and save it as the repo secret NPM_TOKEN.
Release checklist
# 1. Verify the gate
npm run test:all && npm run lint
# 2. Bump the version in package.json (and the app.config.ts debug version)
# 3. Verify the tarball contents
npm pack --dry-run
# 4. Commit, tag, push
git add -A
git commit -m "Release <version>: …"
git tag v<version>
git push origin main --tags
# 5. Publish — either create a GitHub Release (runs CI publish),
# or publish by hand (requires `npm login` as a northwestu org member):
npm publish --access publicAfter publishing, the package appears at https://www.npmjs.com/package/@northwestu/nuxt-nu-design-kit. Consumers install with no auth:
npm install @northwestu/nuxt-nu-design-kitPackage layout
@northwestu/nuxt-nu-design-kit
├── nuxt.config.ts # layer entry (main) — css, fonts, runtime config
├── app.config.ts # typed nDesignKit app config
├── app/
│ ├── assets/css/ # main.css (compat layer), pinned nu-design-system.css
│ ├── components/ # 40 N* Vue wrappers (auto-imported)
│ ├── composables/ # useNFadeIn, useNReel
│ ├── plugins/ # n-design-kit-head.ts (runtime head links), n-interactions.client.ts (fade-in observer)
│ └── utils/ # nClasses
├── scripts/sync-css.sh # re-vendor upstream CSS
├── tests/
│ ├── unit/ # Vitest component + a11y contracts
│ └── e2e/ # Playwright smoke + axe scans
├── vitest.config.ts
├── playwright.config.ts
└── .playground/ # local demo app (not published)Accessibility notes
- Layer sets
html[lang="en"]by default. NListingoverlay links getaria-labelfrom the item title.NReelis a keyboard-focusable region (tabindex="0",role="region") with labelled prev/next controls.NBreadcrumbsusesnav[aria-label="Breadcrumb"]andaria-current="page"; decorative icons arearia-hidden.- Axe color-contrast findings from vendored design-system tokens are reported but not CI-gated until palette work lands upstream.
License
Apache-2.0 — see LICENSE. Source stylesheet: www.northwestu.edu/assets/stylesheets/style-2026.css.
