{
  "$type": "site.standard.document",
  "description": "How I built @colabottles/center-div - a complete journey from problem to published package.",
  "path": "/blog/building-a-production-ready-nuxt-module",
  "publishedAt": "2026-01-17T00:00:00.000Z",
  "site": "at://did:plc:gevyqibw5p2xsonkbsbjm5vy/site.standard.publication/3mnqgth7gxo2f",
  "tags": [
    "post",
    "nuxt",
    "vue",
    "coding"
  ],
  "textContent": "How I built @colabottles/centerdiv  a complete journey from problem to published package\n\n The Impetus\n\nMy background allows me to pick up on programming pretty good these days, that said, I don't have an eidetic memory, I have a 40plus year trove of what I call \"space junk\" floating around in my head, real life things to think about, but I understand the fundamentals and can understand and process by doing and reading. I figured this project is a great little thing to do. So I did it.\n\n The Post I Saw\n\nI was on my usual jaunt through Bluesky seeing if there was anything positive or interesting to read and laid eyes on this comment(https://bsky.app/profile/stephfh.dev/post/3mbmrbat4os2u) and the reply to it by Daniel Roe(https://roe.dev), lead maintainer with the Nuxt core team. I thought to myself, _\"This looks like a funny thing I can do, here is something I can challenge myself with.\"_ and thus I started doing the research.\n\n The ~~Problem~~ Issue That Started It All\n\nIf you've built anything with Nuxt (or anything with anything really), you've probably written this code (or something similar) dozens of times:\n\nhtml\n<div style=\"display: grid; placeitems: center; minheight: 25vh\"\n  <buttonPerfectly Centered</button\n</div\n\n\nor a version of this in your preferred programming language.\n\nIt's such a common pattern—centering content on a page—yet we repeat the same CSS (Cascading Style Sheets) Grid boilerplate over and over. Worse, when you try to abstract it into a component, you often run into hydration mismatches that make your console light up with warnings.\n\nI decided to try and solve this once and for all by building @colabottles/centerdiv, a simple Nuxt module for accessible, hydrationsafe centering. Here's what I learned building and publishing a productionready Nuxt module.\n\n\n\n The Journey: From Idea to npm\n\n Day 1: The Hydration Nightmare\n\nMy first attempt was straightforward—create a Vue component that applies centering styles:\n\nhtml\n<script setup\nconst props = defineProps({\n  minHeight: String\n})\n</script\n\n<template\n  <div\n    :style=\"{\n      display: 'grid',\n      placeItems: 'center',\n      minHeight: minHeight\n    }\"\n  \n    <slot /\n  </div\n</template\n\n\nSimple, right? Wrong. Hydration errors everywhere.\n\nThe problem: Vue was applying styles differently on the server versus the client, causing the dreaded:\n\nbash\nHydration completed but contains mismatches.\n\n\n The Solution: Timing is Everything\n\nAfter researching (and a lot of trial and error), I discovered the issue was about when styles get applied. The fix involved two key changes:\n\n1. Use computed styles instead of reactive object spreads:\n\ntypescript\n<script setup lang=\"ts\"\nimport { computed } from 'vue'\n\nconst computedStyle = computed(() = {\n  const baseStyle: Record<string, string = { display: 'grid' }\n\n  switch (props.axis) {\n    case 'horizontal':\n      baseStyle.justifyItems = 'center'\n      break\n    case 'vertical':\n      baseStyle.alignItems = 'center'\n      break\n    default:\n      baseStyle.placeItems = 'center'\n  }\n\n  if (props.minBlockSize) {\n    baseStyle.minBlockSize = props.minBlockSize\n  }\n\n  return baseStyle\n})\n</script\n\n<template\n  <component :is=\"as\" :style=\"computedStyle\"\n    <slot /\n  </component\n</template\n\n\n2. For the directive, use beforeMount instead of mounted:\n\ntypescript\nexport default defineNuxtPlugin((nuxtApp) = {\n  nuxtApp.vueApp.directive('center', {\n    beforeMount(el: HTMLElement) {\n      el.style.display = 'grid'\n      el.style.placeItems = 'center'\n      el.style.height = '100dvh'\n      el.style.width = '100%'\n    }\n  })\n})\n\n\nResult: Zero hydration warnings. The component rendered identically on server and client.\n\n\n\n Making It Accessible: WCAG (Web Content Accessibility Guidelines) 2.2 Compliance\n\nCentering seems simple, but accessibility matters. I wanted this module to be usable by everyone, including users with disabilities. Here's what I focused on:\n\n 1. Preserving DOM (Document Object Model) Order\n\nMany centering techniques use absolute positioning or flexbox in ways that change visual order without changing DOM order. This breaks screen readers.\n\nMy approach: Pure CSS Grid with placeitems: center. No position manipulation, no reordering.\n\ncss\n.nuxtcenterdiv {\n  display: grid;\n  placeitems: center;\n}\n\n\n 2. Usage of Logical Properties\n\nInstead of minheight, I used minblocksize to respect writing modes and text direction:\n\nhtml\n<CenterDiv minblocksize=\"25vh\"\n  Content\n</CenterDiv\n\n\nThis works correctly for RTL (RighttoLeft) languages and vertical writing modes.\n\n 3. Never Manipulate ARIA (Accessible Rich Internet Applications)\n\nThe component doesn't add any ARIA  attributes, change roles, or trap focus. It's purely presentational—exactly what a layout utility should be. The less the ARIA, the better I say.\n\n 4. Support Semantic HTML (HyperText Markup Language)\n\nThe as prop lets users choose the correct semantic element:\n\nhtml\n< Default: <section \n<CenterDivContent</CenterDiv\n\n< Use <main for main content \n<CenterDiv as=\"main\"Content</CenterDiv\n\n< Use <article for articles \n<CenterDiv as=\"article\"Content</CenterDiv\n\n\nLet's face it, semantic HTML is an art form and one we don't see too often these days that are all about \"build fast and break things\". That is complete malarkey by the way. I'll explain more in a future blog post.\n\nStandards met:\n\n ✅ WCAG 1.3.2  Meaningful Sequence\n ✅ WCAG 1.4.10  Reflow (supports 400% zoom)\n ✅ WCAG 2.4.3  Focus Order\n\n\n\n Testing: The Foundation of Confidence\n\nI learned that tests aren't optional for published packages. Users depend on your code working correctly. Here's the testing stack I built:\n\n Unit Tests (Vitest + Vue Test Utils)\n\ntypescript\nimport { describe, it, expect } from 'vitest'\nimport { mount } from '@vue/testutils'\nimport CenterDiv from '../../src/runtime/components/CenterDiv.vue'\n\ndescribe('CenterDiv', () = {\n  it('renders with default props', () = {\n    const wrapper = mount(CenterDiv, {\n      slots: {\n        default: '<buttonTest</button',\n      },\n    })\n\n    expect(wrapper.find('section').exists()).toBe(true)\n    expect(wrapper.find('button').text()).toBe('Test')\n  })\n\n  it('applies minBlockSize prop', () = {\n    const wrapper = mount(CenterDiv, {\n      props: { minBlockSize: '100vh' },\n    })\n\n    const el = wrapper.element as HTMLElement\n    expect(el.style.minBlockSize).toBe('100vh')\n  })\n})\n\n\nCoverage:\n\n Component rendering\n Props (axis, as, minBlockSize)\n Style application\n 7 tests total\n\n E2E Tests (Playwright)\n\ntypescript\nimport { test, expect } from '@playwright/test'\n\ntest('component renders and centers content', async ({ page }) = {\n  await page.goto('/')\n  await page.waitForLoadState('networkidle')\n\n  const centerDiv = page.locator('.nuxtcenterdiv').first()\n  await expect(centerDiv).toBeVisible()\n\n  const styles = await centerDiv.evaluate((el) = {\n    const computed = window.getComputedStyle(el)\n    return {\n      display: computed.display,\n      placeItems: computed.placeItems,\n    }\n  })\n\n  expect(styles.display).toBe('grid')\n  expect(styles.placeItems).toBe('center')\n})\n\n\nThese tests verify the component works in actual browsers, not just in Node.\n\n Accessibility Tests (vitestaxe)\n\ntypescript\nimport { axe } from 'vitestaxe'\n\nit('has no accessibility violations', async () = {\n  const wrapper = mount(CenterDiv, {\n    slots: {\n      default: '<buttonAccessible Button</button',\n    },\n  })\n\n  const results = await axe(wrapper.element)\n  expect(results.violations).toHaveLength(0)\n})\n\n\nThis caught issues I might have missed, like missing ARIA labels or incorrect heading hierarchy.\n\nFinal count: 11 tests, 100% passing.\n\n\n\n The Module Structure: Following Best Practices\n\nThe official Nuxt module template provides excellent scaffolding. Here's the structure I ended up with:\n\nmd\ncenterdivmodule/\n├── src/\n│   ├── module.ts                     Module registration\n│   └── runtime/\n│       ├── components/\n│       │   └── CenterDiv.vue        Component\n│       ├── plugin.ts                Directive\n│       └── types.ts                 TypeScript definitions\n├── playground/                      Local development\n│   ├── app.vue\n│   └── nuxt.config.ts\n├── test/\n│   ├── unit/\n│   │   └── CenterDiv.test.ts\n│   └── e2e/\n│       └── CenterDiv.spec.ts\n├── package.json\n├── tsconfig.json\n└── vitest.config.ts\n\n\n Key Files Explained\n\nsrc/module.ts  Registers the component and plugin:\n\ntypescript\nimport { defineNuxtModule, addComponent, addPlugin, createResolver } from '@nuxt/kit'\n\nexport default defineNuxtModule({\n  meta: {\n    name: 'centerdiv',\n    configKey: 'centerDiv',\n  },\n  setup(_options, _nuxt) {\n    const resolver = createResolver(import.meta.url)\n\n    addComponent({\n      name: 'CenterDiv',\n      filePath: resolver.resolve('./runtime/components/CenterDiv.vue'),\n    })\n\n    addPlugin({\n      src: resolver.resolve('./runtime/plugin'),\n      mode: 'client',\n    })\n  },\n})\n\n\nplayground/  Critical for development. Run pnpm dev and you get a live Nuxt app with your module loaded. Instant feedback loop.\n\n\n\n The Build Process: Getting Ready for npm (Node PAckage Manager)\n\nBuilding the module involves several steps:\n\n 1. Module Builder\n\nNuxt provides nuxtmodulebuild which handles:\n\n Bundling (ESM (ECMAScript Modules) + CommonJS)\n TypeScript compilation\n Type definitions generation\n\nbash\npnpm run prepack\n\n\nThis creates the dist/ folder:\n\nmd\ndist/\n├── module.mjs               ESM entry\n├── module.cjs               CommonJS entry\n├── module.d.ts              TypeScript definitions\n└── runtime/\n    ├── components/\n    │   └── CenterDiv.vue\n    └── plugin.js\n\n\n 2. Package Configuration\n\nThe package.json tells npm what to publish:\n\njson\n{\n  \"name\": \"@colabottles/centerdiv\",\n  \"version\": \"0.1.2\",\n  \"type\": \"module\",\n  \"exports\": {\n    \".\": {\n      \"types\": \"./dist/types.d.ts\",\n      \"import\": \"./dist/module.mjs\",\n      \"require\": \"./dist/module.cjs\"\n    }\n  },\n  \"files\": \n    \"dist\"\n  \n}\n\n\nOnly the dist/ folder gets published. This keeps the package size tiny (4.4 kB).\n\n 3. Publishing\n\nbash\nnpm publish access public\n\n\nThe access public flag is required for scoped packages (@username/packagename).\n\n\n\n CI/CD: Automating Quality\n\nGitHub Actions runs tests on every push:\n\nyaml\nname: ci\non:\n  push:\n    branches:\n       main\n  pull_request:\n    branches:\n       main\n\njobs:\n  test:\n    runson: ubuntulatest\n    steps:\n       uses: actions/checkout@v6\n       uses: actions/setupnode@v6\n        with:\n          nodeversion: 20\n       run: corepack enable\n       name: Install dependencies\n        run: pnpm install\n       name: Prepare playground\n        run: pnpm run dev:prepare\n       name: Test\n        run: pnpm run test\n\n\nThis catches issues before they reach users.\n\n\n\n Challenges I Faced (And How I Solved Them)\n\n Challenge 1: TypeScript Configuration\n\nProblem: Tests failed in CI because tsconfig.json extended .nuxt/tsconfig.json which doesn't exist until you run the dev server.\n\nSolution: Create a standalone tsconfig.json that doesn't depend on generated files:\n\njson\n{\n  \"compilerOptions\": {\n    \"target\": \"ESNext\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"strict\": true,\n    \"types\": \"node\", \"vitest\"\n  },\n  \"include\": \"src//\", \"test//\"\n}\n\n\n Challenge 2: Vite Version Conflicts\n\nProblem: Vitest and @vitejs/pluginvue used different Vite versions, causing type errors.\n\nSolution: Add a type assertion to ignore the incompatibility:\n\ntypescript\nexport default defineConfig({\n  // @tsexpecterror  Vite version conflict\n  plugins: vue(),\n  test: {\n    environment: 'happydom',\n  }\n})\n\n\n Challenge 3: ESLint Checking Generated Files\n\nProblem: ESLint was throwing 2,825 errors from generated files in playwrightreport/.\n\nSolution: Add ignore patterns to eslint.config.mjs:\n\njavascript\nexport default createConfigForNuxt({\n  // ...\n}).append({\n  ignores: \n    '/dist/',\n    '/node_modules/',\n    '/.nuxt/',\n    '/playwrightreport/',\n    '/testresults/',\n  ,\n})\n\n\n\n\n RealWorld Usage\n\nOnce published, using the module is straightforward:\n\n Installation\n\nbash\nnpm install @colabottles/centerdiv\n\n\n Configuration\n\ntypescript\n// nuxt.config.ts\nexport default defineNuxtConfig({\n  modules: '@colabottles/centerdiv'\n})\n\n\n Component Usage\n\nhtml\n<template\n  < Full viewport centering \n  <CenterDiv minblocksize=\"100vh\"\n    <buttonClick Me</button\n  </CenterDiv\n\n  < Horizontal only \n  <CenterDiv axis=\"horizontal\" minblocksize=\"50vh\"\n    <navNavigation</nav\n  </CenterDiv\n\n  < Custom semantic element \n  <CenterDiv as=\"main\"\n    <h1Main Content</h1\n  </CenterDiv\n</template\n\n\n Directive Usage\n\nhtml\n<template\n  <div vcenter\n    Quick fullviewport centering\n  </div\n</template\n\n\n\n\n What I Learned\n\n 1. Developer Experience Matters\n\nSmall details make a huge difference:\n\n Clear prop names (minBlockSize vs minHeight)\n TypeScript autocomplete\n Helpful README with examples\n Fast development server\n\n 2. Tests Give Confidence\n\nEvery test I wrote caught a real bug. The time invested in testing paid off immediately.\n\n 3. Accessibility Isn't Optional (Which I Already Knew)\n\nBuilding accessible components from the start is easier than retrofitting them later. Using semantic HTML and CSS Grid made accessibility almost automatic.\n\n 4. The Nuxt Ecosystem is Excellent\n\nThe module template, build tools, and documentation made this process smooth. The Nuxt team has done incredible work.\n\n 5. Community Feedback is Essential\n\nReaching out to experienced developers provides invaluable perspective before launching.\n\n\n\n Performance Metrics\n\nThe final package is tiny:\n\nmd\nPackage size: 4.4 kB (compressed)\nUnpacked size: 10.3 kB\nFiles: 15\n\n\nFor comparison, that's smaller than a single mediumresolution image. Zero dependencies beyond Nuxt itself.\n\n\n\n Future Improvements\n\nIdeas for future versions:\n\n1. Gap prop for spacing between centered items:\n\n   html\n   <CenterDiv gap=\"2rem\"\n     <buttonOne</button\n     <buttonTwo</button\n   </CenterDiv\n   \n\n2. Animation support for smooth centering transitions\n\n3. Max inline size for constraining width:\n\n   html\n   <CenterDiv maxinlinesize=\"60ch\"\n     <articleReadable text width</article\n   </CenterDiv\n   \n\n4. Nuxt DevTools integration to visualize all CenterDiv instances on a page\n\n\n\n Try It Yourself\n\nThe module is live on npm:\n\nbash\nnpm install @colabottles/centerdiv\n\n\nLinks:\n\n npm: Click this link to go to the npm listing(https://www.npmjs.com/package/@colabottles/centerdiv)\n GitHub: Click this link to go to the GitHub repository(https://github.com/colabottles/centerdivmodule)\n Documentation: _See README on GitHub_\n\n\n\n Key Takeaways for Building Your Own Module\n\n1. Start with the official template: npx nuxi init mymodule t module\n\n2. Test from day one: Unit, E2E, and accessibility tests\n\n3. Use the playground: Instant feedback beats blind development\n\n4. Focus on DX: Clear APIs, good docs, TypeScript support\n\n5. Make it accessible: Follow WCAG guidelines from the start\n\n6. Keep it small: Every kilobyte matters\n\n7. Automate everything: CI/CD catches issues early\n\n8. Get feedback: Community review prevents bad patterns\n\n\n\n Conclusion\n\nBuilding a Nuxt module taught me more about Vue, TypeScript, testing, and accessibility than any tutorial could. The process of taking an idea from concept to published package—with real users able to npm install it—is incredibly rewarding.\n\nIf you have a common pattern in your Nuxt projects, consider extracting it into a module. The ecosystem benefits, and you'll learn a ton in the process.\n\nWhat patterns do you find yourself repeating in Nuxt? Maybe your next module idea is already in your codebase.\n\n\n\n Resources\n\n Nuxt Module Template:(https://github.com/nuxt/starter/tree/module)\n Nuxt Module Author Guide:(https://nuxt.com/docs/guide/goingfurther/modules)\n WCAG Guidelines:(https://www.w3.org/WAI/WCAG21/quickref/)\n Vitest Documentation:(https://vitest.dev/)\n Playwright Testing:(https://playwright.dev/)",
  "title": "Building a Production-Ready Nuxt Module From Idea to Published"
}