A modern, comprehensive design system built with Nuxt 4, featuring Storybook integration, internationalization support, and modular component architecture.
- 🎨 Component Design System: Built with Vue 3 and Nuxt 4
- 📚 Storybook Integration: Interactive component documentation and testing (using nightly build for Nuxt 4 compatibility)
- 🌍 Multi-language Support: Full i18n with 3 languages (English, Chinese, Arabic)
- 🎯 TypeScript: Full type safety throughout the project
- 🧪 Testing Suite: Comprehensive testing with Vitest and Playwright
- 🔧 Developer Experience: Hot reload, ESLint, and modern tooling
Look at the Nuxt documentation to learn more.
Make sure to install dependencies:
- Check Node version (requires Node.js v22+)
node --version && npm --version- Ensure latest version of Node v22+ using nvm
# Install/update nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
# Install and use Node 22
nvm install 22
nvm use 22# Install dependencies
npm installStart the Nuxt development server on http://localhost:3000:
npm run devThis command automatically:
- Builds and watches i18n translations
- Starts the Nuxt development server
- Enables hot reload for all file changes
⚠️ Note: Storybook is currently functional using a nightly build package (@nuxtjs/storybook@9.1.0-29374011.dab79ae) to ensure compatibility with Nuxt 4.x. This is a temporary solution until the stable release supports Nuxt 4.
Launch Storybook for component development and documentation:
npm run storybook # Starts on http://localhost:6006Storybook will be available at http://localhost:6006/ and includes:
- Interactive component playground with range controls
- Automatic documentation generation
- Accessibility testing tools
- Visual regression testing capabilities
# Development
npm run dev # Start Nuxt with i18n watching
npm run dev:nuxt-only # Start Nuxt without i18n watching
npm run storybook # Start Storybook server (using nightly build)
# Building
npm run build # Build for production
npm run build:i18n # Build i18n translations (TypeScript output)
npm run build:i18n-json # Build i18n translations (JSON output)
npm run build-storybook # Build Storybook for deployment
# Internationalization
npm run build:i18n:watch # Watch and rebuild i18n (TypeScript output)
npm run build:i18n-json:watch # Watch and rebuild i18n (JSON output)
# Utilities
npm run cleanup # Remove node_modules, .nuxt, dist
npm run preview # Preview production buildThis project implements a modern, scalable multi-language support system using @nuxtjs/i18n with component-scoped translations. The application supports three languages with both global and component-specific translation files.
- English (en-GB) - Default language, left-to-right
- Simplified Chinese (zh-CN) - 简体中文, left-to-right
- Arabic (ar-YE) - العربية, right-to-left
The i18n configuration is set up in nuxt.config.ts with:
- Default locale set to English (
en-GB) - Browser language detection enabled
- Cookie-based locale persistence
- Automatic redirection on root path
The project uses a modular translation system with two main sources:
i18n-source/locales/
├── components/
│ └── footer/
│ ├── en-GB.json
│ ├── zh-CN.json
│ └── ar-YE.json
├── global/
│ ├── en-GB.json
│ ├── zh-CN.json
│ └── ar-YE.json
└── pages/
├── account/login/
├── index/
└── settings/locale-switcher/
app/components/
└── header-navigation/
└── locales/
├── en-GB.json
├── zh-CN.json
└── ar-YE.json
The build system automatically merges all translation sources into structured files. The output format depends on which build script is configured:
JSON Output (current dev configuration):
// i18n/locales/en-GB.json
{
"components": {
"footer": {
/* from i18n-source */
},
"navigation": {
/* from component */
}
},
"global": {
/* from i18n-source */
},
"pages": {
/* from i18n-source */
}
}TypeScript Output (alternative configuration):
// i18n/locales/en-GB.ts
export default {
components: {
footer: {
/* from i18n-source */
},
navigation: {
/* from component */
},
},
global: {
/* from i18n-source */
},
pages: {
/* from i18n-source */
},
} as constnpm run build:i18n # Build TypeScript output
npm run build:i18n-json # Build JSON outputnpm run dev # Includes automatic i18n watching and rebuilding (currently configured for JSON output)Available Watch Scripts:
npm run build:i18n:watch- Watches and generates TypeScript filesnpm run build:i18n-json:watch- Watches and generates JSON files
The npm run dev command currently uses the JSON watch script. To switch between formats, update the dev script in package.json to use either build:i18n:watch (TypeScript) or build:i18n-json:watch (JSON).
The build system:
- ✅ Automatically discovers component locale directories
- ✅ Deep merges translations with proper namespacing
- ✅ Watches for changes in both global and component translations
- ✅ Regenerates files with proper structure (JSON or TypeScript)
- ✅ Provides clear logging of discovered directories and changes
Use the $t() function with namespaced keys:
<!-- Global translations -->
<h1>{{ $t("pages.index.header") }}</h1>
<p>{{ $t("global.siteName") }}</p>
<!-- Component translations -->
<span>{{ $t("components.navigation.home") }}</span>
<button>{{ $t("components.footer.settings") }}</button>To add translations for a new component:
-
Create a
locales/directory in your component folder -
Add JSON files for each supported locale (
en-GB.json,zh-CN.json,ar-YE.json) -
Structure translations under a "components" namespace:
{ "components": { "yourComponent": { "title": "Your Title", "description": "Your Description" } } } -
Use in your component:
{{ $t("components.yourComponent.title") }} -
The build system will automatically detect and include your translations
The LocaleSwitcher component (located in app/components/locale-switcher/) provides buttons to switch between languages. It uses the useI18n() composable to:
- Get available locales
- Get current locale
- Switch to a different locale
The language switcher is integrated into the default layout header and automatically updates the page content when a new language is selected.
⚠️ Temporary Setup: Storybook is currently using a nightly build package (@nuxtjs/storybook@9.1.0-29374011.dab79ae) to ensure compatibility with Nuxt 4.x. This setup will be updated to use stable releases once they become available.
This project includes a fully configured Storybook setup for component development and documentation.
- Framework:
@storybook-vue/nuxt(via@nuxtjs/storybooknightly build) - Version: Storybook 10.0.7 with Nuxt 4.2.1 compatibility via nightly package
- Addons:
@storybook/addon-docs10.0.7 - Automatic documentation@storybook/addon-a11y10.0.7 - Accessibility testing@storybook/addon-vitest10.0.7 - Testing integration@chromatic-com/storybook4.1.2 - Visual testing and chromatic integration
Stories are located alongside components:
app/
├── components/
│ └── test-storybook/
│ ├── TestStorybook.vue
│ └── stories/
│ └── TestStorybook.stories.ts
└── storybook/
└── components/
└── display-avatar/
├── DisplayAvatar.stories.ts # With range controls
└── DisplayAvatar.mdx # Documentation
Stories follow the modern Storybook pattern with enhanced controls:
import type { Meta, StoryFn } from "@nuxtjs/storybook"
import MyComponent from "srcdev-nuxt-components/app/components/my-component/MyComponent.vue"
export default {
title: "Components/UI/MyComponent",
component: MyComponent,
argTypes: {
// Range controls for numeric values
size: {
control: { type: "range", min: 1, max: 24, step: 1 },
description: "Component size in pixels",
},
// Select controls for predefined options
variant: {
options: ["primary", "secondary", "tertiary"],
control: { type: "select" },
},
},
args: {
size: 12,
variant: "primary",
},
} as Meta<typeof MyComponent>
const Template: StoryFn<typeof MyComponent> = (args: any) => ({
components: { MyComponent },
setup() {
return { args }
},
template: `<MyComponent v-bind="args" />`,
})
export const Default = Template.bind({})The setup supports various control types:
- Range sliders:
control: { type: "range", min, max, step } - Select dropdowns:
control: { type: "select" }withoptions - Text inputs:
control: { type: "text" } - Boolean toggles:
control: { type: "boolean" } - Color pickers:
control: { type: "color" }
If you encounter Storybook configuration errors:
-
"Could not evaluate @storybook-vue/nuxt" Error:
# Clean install dependencies npm run cleanup npm install -
Version Compatibility Warnings:
- The project uses a nightly build package (
@nuxtjs/storybook@9.1.0-29374011.dab79ae) for Nuxt 4 compatibility - Some peer dependency warnings are expected but don't affect functionality
- This is a temporary solution until stable releases support Nuxt 4
- The project uses a nightly build package (
-
Missing Types: Ensure both
@nuxtjs/storybookand@storybook-vue/nuxtare installed:npm install --save-dev @nuxtjs/storybook @storybook-vue/nuxt
-
Component Not Updating: If controls don't update the component:
- Ensure your Template uses direct prop binding
- Check that args are properly reactive in the template
- i18n Build Errors: Run
npm run build:i18nmanually if translations aren't updating - Port Conflicts: Nuxt uses port 3000, Storybook uses port 6006
- Node Version: Ensure you're using Node.js v22+ for best compatibility
- Storybook Nightly Build: The project temporarily uses
@nuxtjs/storybook@9.1.0-29374011.dab79aefor Nuxt 4 compatibility
If you encounter this error when running npm run dev:
ERROR Could not initialize provider bunny. unifont will not be able to process fonts provided by this provider. Unexpected token '<', "<html><hea"... is not valid JSON
Solution: This error is typically caused by VPN interference with font provider requests. Try turning off your VPN (e.g., Surfshark) and restarting the development server:
# Turn off VPN, then restart
npm run devThe error occurs because VPNs can interfere with the Bunny CDN font provider requests, causing HTML error pages to be returned instead of expected JSON responses.
This project uses @nuxt/content v3.7.1 to provide a file-based content management system. The Content module allows you to write content in Markdown, Vue components, and other formats, and query them through a powerful API.
The Content module is configured in nuxt.config.ts with optimizations for hydration:
modules: [
"@nuxt/content",
// ... other modules
],
content: {
// Configure content to help with hydration
renderer: {
anchorLinks: false,
},
build: {
markdown: {
// Disable plugins that might cause hydration issues
remarkPlugins: {
"remark-slug": false,
"remark-autolink-headings": false,
},
rehypePlugins: {
"rehype-slug": false,
"rehype-autolink-headings": false,
},
},
},
}Content files are stored in the /content/ directory at the project root:
content/
└── about.md # Accessible at /about
- Files in
/content/are automatically mapped to routes about.mdbecomes accessible at/about- Nested directories create nested routes (e.g.,
blog/post1.md→/blog/post1)
All content files support frontmatter for metadata:
---
title: "About us"
description: "Welcome to our site"
bodyClass: "about-us-page"
---
# Your content hereThe project includes custom Vue components that can be used directly in Markdown:
::layout-row{tag=div variant=inset-content style-class-passthrough=mb-20}
:header-block{tag-level=2 class-level=2}[Display Prompt Example]
:raw-text[This renders plain text content]
:markdown-nuxt-link{to="/about" style-class-passthrough="custom-class"}[Link Text]
::The project provides several custom components for enhanced Markdown functionality:
Located at app/components/custom-markdown-components/RawText.vue:
<template>
<slot></slot>
</template>Usage in Markdown:
**Usage in Markdown:**
```markdown
:raw-text[Your plain text content here]Located at app/components/custom-markdown-components/MarkdownNuxtLink.vue:
<template>
<NuxtLink :to :class="[elementClasses]"><slot></slot></NuxtLink>
</template>Usage in Markdown:
**Usage in Markdown:**
```markdown
:markdown-nuxt-link{to="/contact" style-class-passthrough="btn-primary"}[Contact Us]Props:
to(required): The route path
Props:
to(required): The route pathstyle-class-passthrough: CSS classes to apply
The project uses a catch-all route (app/pages/[...slug].vue) to render content dynamically:
<template>
<div>
<NuxtLayout name="default">
<template #layout-content>
<ContentRenderer v-if="page" :value="page" tag="article" :prose="true" />
<div v-else>Page not found</div>
</template>
</NuxtLayout>
</div>
</template>
<script setup lang="ts">
const route = useRoute()
// Query content using the new Content v3 API
const { data: page } = await useAsyncData(`page:${route.path}`, async () => {
const content = await queryCollection("content").path(route.path).first()
return content
})
// Handle 404 errors
if (!page.value) {
throw createError({ statusCode: 404, statusMessage: "Page not found", fatal: true })
}
// Set page metadata from frontmatter
useHead({
title: page.value?.title || "Page",
meta: [
{
name: "description",
content: page.value?.description || "",
},
],
bodyAttrs: {
class: page.value?.bodyClass || "content-page",
},
})
</script>Use the queryCollection API to fetch content:
// Get a single page by path
const page = await queryCollection("content").path("/about").first()
// Get all content
const allContent = await queryCollection("content").all()
// Filter content
const blogPosts = await queryCollection("content")
.path("/blog")
.sort({ date: -1 })
.all()You can also use content in any Vue component:
<script setup lang="ts">
const { data: aboutPage } = await useAsyncData("about", () =>
queryCollection("content").path("/about").first()
)
</script>
<template>
<ContentRenderer v-if="aboutPage" :value="aboutPage" />
</template># Create a new content file
touch content/services.md---
title: "Our Services"
description: "Comprehensive design and development services"
bodyClass: "services-page"
---
# Our Services
We offer a wide range of services...
:raw-text[This is plain text rendered through our custom component]
::layout-row{tag=section variant=popout}
# Service Section
:markdown-nuxt-link{to="/contact"}[Get in touch]
::The file will automatically be accessible at /services
The project includes a custom useMarkdown() composable for processing markdown:
// In app/composables/useMarkdown.ts
export function useMarkdown() {
const renderMarkdown = (text: string): string => {
return md.renderInline(text)
}
return { renderMarkdown }
}Features:
- Automatic external link handling (adds
target="_blank"andrel="noopener noreferrer") - Safe HTML rendering
- Link detection and processing
content/
├── index.md # Homepage content
├── about.md # About page
├── services/
│ ├── index.md # Services overview
│ ├── web-design.md # Individual service
│ └── development.md # Individual service
└── blog/
├── index.md # Blog listing
└── posts/
├── post-1.md
└── post-2.md
Always include essential metadata:
---
title: "Page Title"
description: "SEO-friendly description"
bodyClass: "page-specific-class"
date: "2025-01-15"
author: "Author Name"
---- Use
:raw-text[]for plain text that doesn't need markdown processing - Use
:markdown-nuxt-link{}[]for internal links with custom styling - Wrap related content in layout components using
::component-name{}
The content system automatically handles SEO through:
- Frontmatter title and description become page meta
- Custom body classes for page-specific styling
- Automatic 404 handling for missing content
- File Path Issues: Ensure your content file path matches the URL structure
- Frontmatter Syntax: Check that frontmatter is properly formatted with
---delimiters - Component Errors: Verify custom component syntax matches the component props
The configuration disables certain plugins that can cause hydration mismatches:
remark-slugandrehype-slugare disabledremark-autolink-headingsandrehype-autolink-headingsare disabled
If you experience hydration issues, check the browser console for specific errors.
The project uses a modular CSS architecture located in /app/assets/styles/ with two distinct organizational layers:
The styling system is organized into two main categories:
setup/- Core styles for the current applicationextends-layer/- Layer-specific components and utilities
The setup folder contains all foundational styles for the current application:
_basic-resets.css- Basic CSS resets_head.css- Head-specific styles_normalise.css- CSS normalizationindex.css- Main setup entry point
colors/- Color palette definitions_blue.css,_gray.css,_green.css,_orange.css,_red.css,_yellow.css
themes/- Semantic theme variants_default.css,_primary.css,_secondary.css,_tertiary.css_error.css,_success.css,_warning.css,_info.css,_ghost.css
vars/- Typography variables and scalesutility-classes/- Typography utility classesindex.css- Typography system entry point
_fluid-spacing.css- Responsive spacing utilities_margin.css- Margin utility classes_padding.css- Padding utility classesanimations/- Animation utilities
_utils.css- Accessibility utility classes_variables.css- A11y-focused CSS variablesindex.css- Accessibility system entry point
The extends-layer folder contains styles specific to layer-based components and external design systems:
srcdev-components/- Components from the srcdev layercomponents/- Individual component stylesindex.css- Layer entry point
srcdev-forms/- Form components from the srcdev layersetup/- Form-specific setup and configurationcomponents/- Individual form component stylesindex.css- Form layer entry point
The main stylesheet (main.css) imports in this order:
@import "./setup"; /* Core app styles */
@import "./extends-layer/srcdev-forms"; /* Form layer */
@import "./extends-layer/srcdev-components"; /* Component layer */- Add new core styles to the appropriate
setup/subdirectory - Use
setup/theming/for color and theme definitions - Add utility classes to
setup/utility-classes/ - Include accessibility considerations in
setup/a11y/
- Add component layer styles to
extends-layer/srcdev-components/ - Add form-specific styles to
extends-layer/srcdev-forms/ - Create new layer directories in
extends-layer/for additional design systems
This architecture ensures clear separation between application-specific styles and reusable layer components, making the design system modular and maintainable.
srcdev-design-system/
├── .storybook/ # Storybook configuration (deprecated)
├── app/ # Nuxt application
│ ├── components/ # Vue components ~~with stories~~ (stories deprecated)
│ ├── composables/ # Vue composables
│ ├── layouts/ # Nuxt layouts
│ ├── middleware/ # Route middleware
│ ├── pages/ # Application pages
│ └── stores/ # Pinia stores
├── components/ # Legacy components (to be migrated)
├── i18n/ # Generated i18n files
├── i18n-source/ # Source i18n translations
├── public/ # Static assets
├── scripts/ # Build scripts
├── server/ # Server-side code
└── types/ # TypeScript definitions
- Component Development: Add new components in
app/components/with corresponding stories(Storybook stories temporarily deprecated) - Internationalization: Add translations in component
locales/directories - Testing: Write tests for all new components and functionality
- Documentation:
Update Storybook stories andUpdate README when adding features
Build the application for production:
npm run buildPreview production build locally:
npm run previewCheck out the Nuxt deployment documentation for more information.
Built with ❤️ using Nuxt 4, Vue 3~~, and Storybook~~