srcdev-nuxt-components
SRCDEV Nuxt Components
NOTE
Although this repo is public and feel free to do what you wish with it, this has been developed for use with websites we develop.
Install Nuxt Components layer
npm install --save srcdev-nuxt-components
defineNuxtConfig({
extends: "srcdev-nuxt-components",
css: ["srcdev-nuxt-components/app/assets/styles/main.css", "./app/assets/styles/main.css"],
});
Note: The layer CSS is not automatically included when installed from
node_modules. You must explicitly add it to thecssarray as shown above. The second entry (./app/assets/styles/main.css) is your app's own stylesheet for overrides — create it if it doesn't exist.
Claude Code Skills
This package ships Claude Code skills (reference docs for components and development tasks) and a curated set of Claude Code rules (short constraints for working with the layer: CSS nesting, overrides, props, NuxtImg, Storybook args) in the .claude/ directory.
To make them available in your project, add this script to your package.json:
"setup:claude": "rm -rf .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && mkdir -p .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/rules/consumer/. .claude/rules/srcdev-nuxt-components"
Then run it after install:
npm run setup:claude
Skills are copied into .claude/skills/srcdev-nuxt-components/ and rules into .claude/rules/srcdev-nuxt-components/, so they never conflict with or overwrite your own project's. Re-running the script after a package update is safe: it replaces only those two srcdev-nuxt-components folders, so skills and rules dropped or renamed upstream are removed too, and your own skills and rules are never touched.
Automate with postinstall (recommended)
To ensure skills are always up to date, VSCode snippets are installed, and nuxt prepare is never forgotten, add a postinstall script. npm runs this automatically after every npm install:
"scripts": {
"setup:claude": "rm -rf .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && mkdir -p .claude/skills/srcdev-nuxt-components .claude/rules/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/skills/. .claude/skills/srcdev-nuxt-components && cp -r node_modules/srcdev-nuxt-components/.claude/rules/consumer/. .claude/rules/srcdev-nuxt-components",
"postinstall": "node node_modules/srcdev-nuxt-components/scripts/copy-snippets.mjs && nuxt prepare && npm run setup:claude"
}
What this does:
copy-snippets.mjs— copies VSCode snippets (.code-snippetsfiles) from the layer to your.vscode/foldernuxt prepare— generates Nuxt type declarationssetup:claude— copies skills into.claude/skills/srcdev-nuxt-components/and rules into.claude/rules/srcdev-nuxt-components/
Note: The snippet copy must run from your consumer app's postinstall, not the layer's. npm doesn't invoke postinstall hooks for dependencies, so each consumer app is responsible for copying snippets into its own
.vscode/folder. Don't setSRCDEV_STANDALONEin a consumer app: that flag is only for running this layer on its own (it enables dev-only modules and the ramp watcher).
Scaffolding a New App
A Claude Code skill is included to scaffold a new Nuxt consumer app from scratch. It generates
package.json, nuxt.config.ts, ESLint/Prettier config, the full app/ directory structure,
and a CLAUDE.md — all pre-wired to extend this layer correctly.
Trigger it by saying to Claude Code:
"Scaffold a new layer consumer app. Repo:
/path/to/repo, name:my-app, domain:myapp.co.uk, fonts:Fraunces, Manrope."
The skill is available at .claude/skills/new-app-scaffold.md once copied into your project
via npm run setup:claude.
Components
Components live in app/components/, grouped into tiers:
| Folder | Storybook title prefix |
|---|---|
01.atoms |
Atoms/ |
02.molecules |
Molecules/ |
03.organisms |
Organisms/ |
04.templates |
Templates/ |
05.forms |
Forms/ |
Components are auto-imported with pathPrefix: false, so the tier folder never appears in the tag name.
Naming: a bare form control is <Name> (e.g. InputNumber) and its labelled wrapper is <Name>Field (e.g. InputNumberField). Older Core / Default / WithLabel suffixes are being renamed to this convention; see .claude/skills/component-naming.md for the backlog.
Each component ships with:
CONSUMER-STYLING.mdnext to the.vuefile, listing its public CSS custom properties and how to override them- a Storybook story, the only place components are demonstrated (this repo has no demo pages)
- a skill doc in
.claude/skills/components/, copied into your app bynpm run setup:claude - a VS Code snippet in
.vscode/srcdev-component-{name}.code-snippets, copied into your app bypostinstall
Consumer App Configuration
Configuration options for apps extending this layer. All options go in the consumer's nuxt.config.ts under runtimeConfig.public and can also be set via environment variable.
Colour Scheme
The layer ships with light/dark/auto colour scheme support. This includes a synchronous <head> script (FOUC prevention) and the useColourScheme() composable.
Consumer apps that only use a single default scheme can disable it entirely:
// nuxt.config.ts
runtimeConfig: {
public: {
colourScheme: {
enabled: false, // disables head script injection and composable side effects
},
},
},
Or via environment variable:
NUXT_PUBLIC_COLOUR_SCHEME_ENABLED=false
When disabled, no data-color-scheme attribute is set on <html> and useColourScheme() is a no-op. The default is true.
See .claude/skills/colour-scheme-disable.md for the full guide.
Analytics
Call useAnalytics() from a layout, page or component and use trackEvent(name, params) to fire events. Only Google Analytics (GA4) is implemented today; call sites never name the provider, so adding another later won't change them.
// nuxt.config.ts
runtimeConfig: {
public: {
analytics: {
provider: "google-analytics",
googleAnalytics: { id: "G-XXXXXXXXXX" },
},
},
},
Or via environment variable:
NUXT_PUBLIC_ANALYTICS_GOOGLE_ANALYTICS_ID=G-XXXXXXXXXX
With no ID set, trackEvent does nothing.
useWhatsApp().openWhatsApp(fields) opens a WhatsApp chat pre-filled with the given label/value pairs. Set the number in international format, without + or spaces:
NUXT_PUBLIC_WHATSAPP_NUMBER=447700900000
Without a number it logs a warning and does nothing.
Colour System
The layer ships a parametric oklch colour ramp. Two CSS custom properties — --theme-hue and
--theme-chroma — drive an 11-step colour scale (--colour-theme-0 to --colour-theme-10) that
all themed components (buttons, inputs, prompts, toasts) read through a shared set of semantic
slots. Switching the palette is a two-variable change; no token-by-token remapping is required.
How it works
A formula declared on every potential theme host (html, [data-theme], [data-invalid])
computes the scale from the two params:
--colour-theme-6: oklch(56% calc(var(--theme-chroma) * 1) var(--theme-hue)); /* peak chroma */
--colour-theme-0: oklch(98% calc(var(--theme-chroma) * 0.045) var(--theme-hue)); /* near-white */
--colour-theme-10: oklch(25% calc(var(--theme-chroma) * 0.64) var(--theme-hue)); /* near-black */
Scale direction: 00 = lightest, 10 = darkest. Chroma tapers at both ends and peaks at step 06.
Nine semantic slots (--theme-surface, --theme-accent, --theme-text, --theme-ring, etc.) are derived from the
scale and are shared by all components. They ship light values only (no light-dark(), which older iPad
Safari lacks); dark mode is left to the consuming app, see .claude/skills/theming-dark-mode.md. Theme variants (data-theme="success",
"warning", "error") swap --theme-hue and --theme-chroma on their element; the formula
re-evaluates locally so each themed element gets its own full palette without affecting the page.
Built-in named palettes
| Name | Hue | Used for |
|---|---|---|
| blue | 255 | Default (page-level) |
| red | 30 | Error / data-theme="error" |
| green | 157 | Success / data-theme="success" |
| amber | 75 | Available for consumer use |
| orange | 55 | Warning / data-theme="warning" |
| sunset | 50 | Available for consumer use (with hue drift) |
| slate | 260 | Near-neutral grey |
Consumer app: generating a custom palette
The recommended approach for a consuming app is to generate your own named colour files. This gives
you clean step variables (--gold-09, --gold-04) rather than raw oklch values in your theme
overrides.
1. Create ramps.config.mjs in your project root
Define the palettes you want. You can add new ones, reuse a built-in name to override the layer's values, or do both. Consumer CSS loads after the layer, so your generated files win the cascade automatically:
// ramps.config.mjs
export const ramps = {
// New palette — adds --gold-00..10 and --palette-gold-* vars
gold: { hue: 85, chroma: 0.2 },
// Override a built-in — replaces the layer's --blue-00..10 with your values
// blue: { hue: 240, chroma: 0.18 },
// Override the error palette — all error/invalid states use your red
// red: { hue: 15, chroma: 0.26 },
// Optional — add hue drift to rotate colour across the scale:
// copper: { hue: 45, chroma: 0.21, drift: -15 },
};
2. Add the generator script to package.json
The script lives in the layer's node_modules — no copying required. Prepending it to dev,
build, and generate prevents generated CSS from drifting out of sync with ramps.config.mjs:
"scripts": {
"generate:ramps": "node node_modules/srcdev-nuxt-components/scripts/generate-consumer-ramps.mjs",
"dev": "npm run generate:ramps && nuxt dev",
"build": "npm run generate:ramps && nuxt build",
"generate": "npm run generate:ramps && nuxt generate"
}
Do not manually edit generated files. They carry a
/* GENERATED */comment at the top and are rebuilt every time the generator runs. Put all changes inramps.config.mjsinstead.
3. Run it
npm run generate:ramps
Produces in app/assets/styles/setup/02.colours/:
_gold.css—--gold-00…--gold-10(literal oklch values)_palette-params.css—--palette-gold-hue,--palette-gold-chroma
4. Import the generated files
In your app/assets/styles/setup/02.colours/index.css (create if it doesn't exist):
@import "./_palette-params";
@import "./_gold";
5. Set the palette as your theme default
/* app/assets/styles/setup/03.theming/_default.css */
:where(html) {
--theme-hue: var(--palette-gold-hue);
--theme-chroma: var(--palette-gold-chroma);
/* Page-level tokens — readable named steps, not raw oklch */
--colour-text-accent: var(--gold-09);
--colour-text-eyebrow: var(--gold-09);
}
6. Wire up in your setup index
/* app/assets/styles/setup/index.css */
@import "./02.colours/";
@import "./03.theming/_default.css";
And in app/assets/styles/main.css:
@import "./setup/";
Hue quick reference
| Range | Colour |
|---|---|
| 0–30 | Red / pink |
| 30–70 | Orange / amber |
| 70–100 | Yellow / gold |
| 100–160 | Green |
| 220–270 | Blue |
| 270–310 | Violet |
Use oklch.com to preview values before committing.
Generator (for library contributors)
Named palettes in the layer itself are maintained in ramps.config.mjs at the repo root:
npm run generate:ramps # rebuild all layer CSS from ramps.config.mjs
npm run check:ramps # CI: fail if generated CSS is out of date
Further reading
| Guide | Location |
|---|---|
| Full ramp architecture, formula details, hue drift | .claude/skills/theming-colour-ramps.md |
| Full palette swap for a consumer app | .claude/skills/theming-override-default.md |
| Partial token override (palette shift, buttons, inputs) | .claude/skills/theming-partial-override.md |
| Add a dark scheme (layer ships light values only) | .claude/skills/theming-dark-mode.md |
| Disable light/dark mode support | .claude/skills/colour-scheme-disable.md |
Skills are available in your project after running npm run setup:claude.
Development Environment (.vscode)
The .vscode directory contains Visual Studio Code configuration files to ensure a consistent development experience across the project:
Workspace Configuration
settings.json- VS Code workspace settings including:- Code formatting configuration (2-space indentation, auto-formatting on save)
- ESLint, Prettier, and Stylelint integration
- File handling settings (trim whitespace, final newlines, Unix line endings)
- CSS variable recognition for the project's custom properties
Recommended Extensions
extensions.json- Curated list of VS Code extensions for optimal development:- Vue.js Development:
vue.volar- Vue 3 language support - Nuxt.js Development:
nuxtr.nuxtr-vscode- Enhanced Nuxt development tools - Code Quality:
dbaeumer.vscode-eslint,esbenp.prettier-vscode- Linting and formatting - CSS Development:
willofindie.vscode-cssvar- CSS custom property IntelliSense - Testing:
vitest.explorer- Vitest test runner integration - Markdown:
davidanson.vscode-markdownlint- Markdown linting - Productivity:
jkjustjoshing.vscode-text-pastry,formulahendry.auto-rename-tag,nixon.env-cmd-file-syntax - AI-assisted development:
anthropic.claude-code
- Vue.js Development:
Code Snippets
Every component ships a matching .vscode/srcdev-component-{name}.code-snippets file — one per
component, kept up to date as part of this project's component migration workflow (see
CLAUDE.md). Consumer apps get all of them copied into their own .vscode/ folder automatically
via the postinstall setup described above; there's nothing to configure manually.
Contact Form — Resend Setup
The ContactSection component (see it in Storybook under Molecules/ContactSection) sends enquiries via Resend.
No extra packages are required — the server route calls the Resend REST API directly.
1. Create a Resend account
Sign up at resend.com. The free tier allows 3,000 emails/month (100/day), which is more than sufficient for a contact form.
2. Verify a sending domain
In the Resend dashboard go to Domains → Add Domain and follow the DNS instructions for your
domain. This is required before you can send from a custom from address in production.
Local development shortcut: you can skip domain verification and use Resend's shared sandbox address
onboarding@resend.devas thefromaddress. Emails will only be delivered to the email address registered on your Resend account, so it is safe for testing.
3. Create an API key
In the Resend dashboard go to API Keys → Create API Key. Copy the key — it is only shown once.
4. Configure environment variables
Copy .env.example to .env and fill in the three values:
cp .env.example .env
NUXT_RESEND_API_KEY=re_xxxxxxxxxxxxxxxxxxxxxxxxxxxx
NUXT_CONTACT_EMAIL_TO=you@yourdomain.com
NUXT_CONTACT_EMAIL_FROM=Enquiries <hello@yourdomain.com>
| Variable | Description |
|---|---|
NUXT_RESEND_API_KEY |
API key from the Resend dashboard |
NUXT_CONTACT_EMAIL_TO |
The inbox that receives enquiries |
NUXT_CONTACT_EMAIL_FROM |
The "from" address shown to recipients — must use a verified Resend domain |
Nuxt maps NUXT_* variables to runtimeConfig automatically at runtime. The values are
server-only and never included in the client bundle.
5. Vercel deployment
Rather than committing a .env file, add the variables directly in the Vercel dashboard:
- Open your project in vercel.com
- Go to Settings → Environment Variables
- Add each of the three
NUXT_*variables, setting the environment to Production (and Preview if you want the form to work on preview deployments too) - Redeploy — Vercel injects the variables at build and runtime automatically
.envis listed in.gitignoreand should never be committed to the repository.
Testing
This project has two test layers that run independently.
Unit & Snapshot Tests (Vitest)
Runs component logic, HTML structure, and snapshot regression tests. No browser or running server required.
# Run in watch mode (development)
npm run test
# Run once (CI / pre-commit)
npm run test:run
# Open the Vitest UI (browser-based test explorer)
npm run test:ui
# Update snapshots after an intentional component change
npm run test:update
Visual Regression Tests (Playwright)
Runs pixel-level screenshot comparisons against Storybook. Requires Storybook to be running first. Coverage is currently limited to a few components (InputButton, HeroText, EyebrowText, ContentDocs); specs live in each component's playwright/ folder as *.playwright.ts.
# 1. Start Storybook
npm run storybook:serve
# 2. In a separate terminal, run visual tests
npm run playwright
# Update visual baselines after an intentional visual change
npm run playwright:update
# View Playwright test report
npx playwright show-report
Visual tests run across Chromium, Firefox, and WebKit. Snapshot baselines are stored per browser — expect three PNG files per component test.
What Each Layer Catches
| Change | Unit tests | Visual tests |
|---|---|---|
| Class added / removed | ✅ | ✅ |
| HTML structure changed | ✅ | ✅ |
| Font weight / color / spacing changed | ❌ | ✅ |
| Prop or slot logic broken | ✅ | ❌ |
| Accessibility attribute missing | ✅ | ❌ |
Storybook
Storybook (v10, via @nuxtjs/storybook) is the only demo surface for this library, and the target for visual regression tests. The deployed build is at storybook.srcdev.co.uk.
Scripts
# Start Storybook dev server (http://localhost:6006). `npm run dev` does the same after
# regenerating the colour ramps; there is no `nuxt dev` entry point.
npm run storybook
# Build a static Storybook (outputs to storybook-static/)
npm run storybook:build
# Build and serve locally — used before running Playwright visual tests
npm run storybook:serve
# Clear Storybook and Vite caches — run this if styles appear stale after changes
npm run storybook:cache:clean
After clearing the cache, restart with
npm run storybook. The cache clear is particularly useful when changes inside@layerCSS blocks are not reflected in the running dev server.
MCP server for AI agents
@storybook/addon-mcp exposes the stories over MCP while the dev server is running, so a coding agent can look up components, their props and story examples instead of guessing. The endpoint is http://localhost:6006/mcp. To connect Claude Code:
claude mcp add --transport http storybook http://localhost:6006/mcp
See the Storybook MCP docs for other clients.
Fonts
@nuxt/fonts is disabled in Storybook (detected via process.env.STORYBOOK in nuxt.config.ts).
Fonts are served instead from local files in .storybook/public/_fonts/, declared in .storybook/fonts.css and imported in .storybook/preview.ts.
| Font | Format | Source |
|---|---|---|
| Poppins | TTF | .storybook/public/_fonts/poppins/ |
| Playfair Display | woff2 | .storybook/public/_fonts/playfair-display/ |
| Mono MMM 5 | TTF | public/fonts/ |
To add a new font, see .claude/skills/storybook-add-font.md.
Version History
| Version | Released | License |
|---|---|---|
| 9.6.20 | MIT | |
| 9.6.19 | MIT | |
| 9.6.18 | MIT | |
| 9.6.17 | MIT | |
| 9.6.16 | MIT | |
| 9.6.15 | MIT | |
| 9.6.14 | MIT | |
| 9.6.13 | MIT | |
| 9.6.12 | MIT | |
| 9.6.11 | MIT | |
| 9.6.10 | MIT | |
| 9.6.9 | MIT | |
| 9.6.8 | MIT | |
| 9.6.7 | MIT | |
| 9.6.6 | MIT | |
| 9.6.5 | MIT | |
| 9.6.4 | MIT | |
| 9.6.3 | MIT | |
| 9.6.2 | MIT | |
| 9.6.1 | MIT | |
| 9.6.0 | MIT | |
| 9.5.7 | MIT | |
| 9.5.6 | MIT | |
| 9.5.5 | MIT | |
| 9.5.4 | MIT | |
| 9.5.3 | MIT | |
| 9.5.2 | MIT | |
| 9.5.1 | MIT | |
| 9.5.0 | MIT | |
| 9.4.12 | MIT |
Showing the latest 30 of 335. See every release on npm