{"id":15,"date":"2026-06-06T21:48:30","date_gmt":"2026-06-06T21:48:30","guid":{"rendered":"https:\/\/softwareproduction.eu\/wordpress\/?p=15"},"modified":"2026-06-07T01:21:52","modified_gmt":"2026-06-07T01:21:52","slug":"building-a-headless-design-system-in-vue-3-the-compose-pattern","status":"publish","type":"post","link":"https:\/\/softwareproduction.eu\/?p=15","title":{"rendered":"Building a Headless Design System in Vue 3 \u2014 The Compose Pattern"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><em>Eighth in a series about migrating from legacy architectures to a modern Nuxt 4 stack.<\/em><\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Problem with Traditional Component Libraries<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In most Vue component libraries, styling logic lives inside the <code>.vue<\/code> file \u2014 in scoped CSS, computed class strings, or inline styles. That creates three recurring problems:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Untestable styles<\/strong> \u2014 unit testing a <code><style scoped><\/code> block requires mounting the component in a DOM<\/li>\n<li><strong>Opaque to tooling<\/strong> \u2014 AI assistants, documentation generators, and design systems cannot introspect scoped styles<\/li>\n<li><strong>Tight coupling<\/strong> \u2014 changing a variant or adding a size means editing the <code>.vue<\/code> file, which mixes style changes with template changes<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">These issues compound quickly in large design systems. With 60+ components and 200+ variants in a large enterprise application, every style change risks breaking the template, and every template change risks affecting styles.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Compose Pattern: Separation of Concerns<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The compose pattern splits each component into two files:<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  subgraph Traditional\n    A[Button.vue]\n    A --&gt; A_t1[\"&amp;lt;template&amp;gt;\"]\n    A_t1 --&gt; A_btn[\"&amp;lt;button :class&amp;gt; &amp;lt;slot \/&amp;gt; &amp;lt;\/button&amp;gt;\"]\n    A_btn --&gt; A_t2[\"&amp;lt;\/template&amp;gt;\"]\n    A --&gt; A_s1[\"&amp;lt;script setup&amp;gt; \/\/ props + logic &amp;lt;\/script&amp;gt;\"]\n    A --&gt; A_st[\"&amp;lt;style scoped&amp;gt; .btn, .btn-solid, .btn-sm, ... &amp;lt;\/style&amp;gt;\"]\n  end\n\n  subgraph Compose_File[composeButton.ts]\n    B_if[\"interface ButtonProps\\n- variant: 'solid' #124; 'outline' #124; 'ghost'\\n- size: 'sm' #124; 'md'\\n- color: 'primary' #124; 'secondary'\"]\n    B_fn[\"function classes(props: ButtonProps): string\\n\u2192 'btn btn-${variant} btn-${size} btn-${color}'\"]\n  end\n\n  subgraph ThinWrapper[Button.vue]\n    C_t[\"&amp;lt;template&amp;gt; &amp;lt;button :class&amp;gt; &amp;lt;slot \/&amp;gt; &amp;lt;\/button&amp;gt; &amp;lt;\/template&amp;gt;\"]\n    C_s[\"&amp;lt;script setup&amp;gt;\\nimport { compose }\\nconst classes = computed(() =&amp;gt; compose(props))\\n&amp;lt;\/script&amp;gt;\"]\n  end<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The compose file is <strong>pure TypeScript<\/strong> \u2014 no Vue, no DOM, no scoped styles. It exports:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>A <strong>TypeScript interface<\/strong> that defines the component\u2019s API, or props<\/li>\n<li>A <strong>pure function<\/strong> that maps props to CSS class strings<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The <code>.vue<\/code> file becomes a thin wrapper: it calls the compose function and renders the result.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Why This Separation Matters<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">1. Unit-Testable Styles<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The compose function is pure: given props, it returns a class string. Testing it requires no DOM, no <code>mount()<\/code>, and no <code>@vue\/test-utils<\/code>:<\/p>\n\n\n\n<pre><code class=\"language-typescript\">\/\/ composeButton.test.ts\nimport { classes } from '.\/composeButton'\n\ntest('solid primary button', () =&gt; {\n  expect(classes({ variant: 'solid', color: 'primary', size: 'md' }))\n    .toBe('btn btn-solid btn-primary btn-md')\n})\n\ntest('outline danger button', () =&gt; {\n  expect(classes({ variant: 'outline', color: 'danger', size: 'sm' }))\n    .toBe('btn btn-outline btn-danger btn-sm')\n})<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">By contrast, testing scoped styles means mounting the component, rendering it into a DOM, and inspecting computed styles \u2014 a process that is slower, more fragile, and dependent on the environment.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">2. Transparent to AI Assistants<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Given <code>composeButton.ts<\/code>, an AI can immediately understand the full component API \u2014 every valid variant, size, and color. With scoped styles, the AI would have to infer valid combinations by parsing CSS class names.<\/p>\n\n\n\n<pre><code class=\"language-text\">AI sees composeButton.ts:\n  \"Button accepts variant (solid|outline|ghost),\n   size (sm|md|lg), color (primary|secondary|danger).\n   I can generate correct usage without looking at the template.\"<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">3. Figma-to-Code Alignment<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Design tools like Figma define components through properties: variant, size, and color. The compose file\u2019s TypeScript interface mirrors that structure exactly. The mapping is one-to-one:<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  subgraph Figma[\"Design (Figma)\"]\n    F[Component: Button]\n    F --&gt; Fv[Property: Variant = Solid]\n    F --&gt; Fs[Property: Size = Medium]\n    F --&gt; Fc[Property: Color = Primary]\n  end\n\n  subgraph Code[\"Code (TypeScript)\"]\n    C[interface ButtonProps]\n    C --&gt; Cv[\"variant: 'solid' #124; 'outline'\"]\n    C --&gt; Cs[\"size: 'sm' #124; 'md'\"]\n    C --&gt; Cc[\"color: 'primary' #124; 'secondary'\"]\n  end\n\n  Fv -.1:1 mapping.- Cv\n  Fs -.1:1 mapping.- Cs\n  Fc -.1:1 mapping.- Cc<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">When designers and developers share the same vocabulary, handoff friction drops. In that sense, the compose file <em>is<\/em> the specification.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">4. Composability Across Rendering Targets<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The compose function does not depend on Vue. It produces CSS class strings that work in any context \u2014 Vue components, server-rendered HTML, email templates, marketing pages, internal tools, and documentation sites.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  P[\"compose(props): classes string\"]\n\n  P --&gt; V[Vue components]\n  P --&gt; S[SSR templates]\n  P --&gt; E[Email templates]\n  P --&gt; M[Marketing pages]\n  P --&gt; I[Internal tools]\n  P --&gt; D[Docs site]\n\n  class P highlight;<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">A Real Example: The Card Component<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A card component with header, body, footer, and multiple variants:<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  subgraph ComposeCard[composeCard.ts]\n    direction TB\n    CP[\"CardProps\\n- variant: 'elevated' #124; 'outlined' #124; 'flat'\\n- padding: 'none' #124; 'sm' #124; 'md' #124; 'lg'\\n- rounded: boolean\\n- interactive: boolean\"]\n    CS[\"CardSlots\\n- header?: { classes }\\n- default: { classes }\\n- footer?: { classes }\"]\n    CF[\"compose(props: CardProps)\\n\u2192 { root, header, body, footer } class map\"]\n\n    CP --&gt; CF\n    CS --&gt; CF\n  end\n\n  subgraph CardVue[Card.vue]\n    direction TB\n    T[\"&amp;lt;template&amp;gt;\\n  &amp;lt;div :class='styles.root'&amp;gt;\\n    &amp;lt;div v-if='$slots.header' :class='styles.header'&amp;gt;\\n      &amp;lt;slot name='header' \/&amp;gt;\\n    &amp;lt;\/div&amp;gt;\\n    &amp;lt;div :class='styles.body'&amp;gt;\\n      &amp;lt;slot \/&amp;gt;\\n    &amp;lt;\/div&amp;gt;\\n    &amp;lt;div v-if='$slots.footer' :class='styles.footer'&amp;gt;\\n      &amp;lt;slot name='footer' \/&amp;gt;\\n    &amp;lt;\/div&amp;gt;\\n  &amp;lt;\/div&amp;gt;\\n&amp;lt;\/template&amp;gt;\"]\n    S[\"&amp;lt;script setup&amp;gt;\\nconst props = defineProps&amp;lt;CardProps&amp;gt;()\\nconst styles = computed(() =&amp;gt; compose(props))\\n&amp;lt;\/script&amp;gt;\"]\n  end\n\n  CF --&gt; T\n  CF --&gt; S<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The compose function returns a <strong>styles object<\/strong> \u2014 a named map of classes for each DOM region. The template can apply styles to each part independently, which is more flexible than a flat class string.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Integration with Design Tokens<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The compose pattern works naturally with design tokens, whether they are CSS custom properties or Tailwind theme values:<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  subgraph Tokens[Design Tokens]\n    TP[\"--color-primary\"]\n    TS[\"--spacing-md: 16px\"]\n    TR[\"--radius-lg: 12px\"]\n  end\n\n  subgraph ComposeFn[Compose Function]\n    CP[\"color: 'primary' \u2192 'text-primary bg-primary\/10'\"]\n    CS[\"padding: 'md' \u2192 'p-4' (maps to 16px)\"]\n    CR[\"rounded \u2192 'rounded-lg'\"]\n  end\n\n  TP --&gt; CP\n  TS --&gt; CS\n  TR --&gt; CR<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The compose function maps design tokens to concrete CSS classes. Changing a token value \u2014 for example, updating the primary color \u2014 propagates through every component that uses it, because the function references tokens by name rather than by value.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">The Pattern at Scale<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">In a large enterprise design system with dozens of components using the compose pattern:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><thead><tr><th>Metric<\/th><th>Traditional<\/th><th>Compose Pattern<\/th><\/tr><\/thead><tbody><tr><td><strong>Style tests<\/strong><\/td><td>Require DOM mounting<\/td><td>Pure function tests<\/td><\/tr><tr><td><strong>Variant coverage<\/strong><\/td><td>Often incomplete<\/td><td>Exhaustive (type system enforces it)<\/td><\/tr><tr><td><strong>AI component usage<\/strong><\/td><td>Error-prone guessing<\/td><td>Exact API knowledge from interface<\/td><\/tr><tr><td><strong>Design-dev alignment<\/strong><\/td><td>Manual translation<\/td><td>1:1 property mapping<\/td><\/tr><tr><td><strong>Refactoring risk<\/strong><\/td><td>High (style \u2194 template coupling)<\/td><td>Low (independent files)<\/td><\/tr><tr><td><strong>Documentation<\/strong><\/td><td>Manual, often stale<\/td><td>Auto-generated from interfaces<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  subgraph Traditional_DS[Traditional Design System]\n    T1[\"Styles &amp; templates coupled\"]\n    T2[DOM-dependent tests]\n    T3[Implicit contracts]\n  end\n\n  subgraph Compose_DS[Compose Pattern System]\n    C1[Pure compose functions]\n    C2[Type-enforced variants]\n    C3[Auto-doc + AI-friendly]\n  end\n\n  T1 --&gt;|migrate| C1\n  T2 --&gt;|refactor tests| C2\n  T3 --&gt;|extract interfaces| C3<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">Lessons Learned<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Pure functions are the most powerful abstraction in frontend<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">A function from props to class strings is testable, composable, transparent to tooling, and framework-agnostic. It is the simplest possible API, and often the strongest.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The interface is the contract, not the template<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In a traditional component, the contract is implicit \u2014 you learn the API by reading the template. In the compose pattern, the TypeScript interface <em>is<\/em> the contract. Autocompletion exposes every valid prop combination without requiring you to inspect the implementation.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  TI[\"TypeScript interface\\n(e.g., ButtonProps)\"] --&gt; IDE[IDE autocomplete\\n+ tooling]\n  TI --&gt; Docs[Generated docs]\n  TI --&gt; AI[\"AI assistants\\n(exact prop space)\"]\n  TI --&gt; Impl[\"compose(props): classes\"]\n\n  Impl --&gt; Templates[Thin Vue templates]<\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Scoped styles are the wrong default for design systems<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Scoped styles work for application-specific components that are never reused or tested independently. For design system components \u2014 where consistency, testability, and tooling integration matter \u2014 the compose pattern is a better fit.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The compose pattern is not class-name engineering<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The value is not in class-name concatenation. It is in the <strong>separation<\/strong>: the interface defines the API, the function implements the mapping, and the template stays a thin shell. That separation enables testing, AI integration, design alignment, and documentation.<\/p>\n\n\n\n<pre class=\"mermaid\">flowchart LR\n  I[\"Props interface\\n(API contract)\"] --&gt; F[\"Compose function\\n(props \u2192 class map)\"]\n  F --&gt; Vt[\"Vue template\\n(thin shell)\"]\n  F --&gt; Tst[\"Unit tests\\n(pure functions)\"]\n  F --&gt; Tooling[AI + Docs + Design tools]\n\n  class I,F,Vt,Tst,Tooling highlight;<\/pre>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">What's Next<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Article 19<\/strong>: <em>A\/B Testing at the SSR Level \u2014 Cookie-Based Variant Selection<\/em> \u2014 How to serve different content variants during server rendering.<\/li>\n<li><strong>Article 20<\/strong>: <em>Contentful Live Preview \u2014 Real-Time WYSIWYG Editing in SSR<\/em> \u2014 Making the CMS preview iframe work with server-side rendering.<\/li>\n<li><strong>Article 21<\/strong>: <em>Structured Logging in Nuxt \u2014 From <code>console.log<\/code> to Production Observability<\/em> \u2014 Multi-sink logging with runtime control.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Munir Husseini is a software architect specializing in full-stack TypeScript, .NET, and cloud-native architectures.<\/em><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Eighth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. The Problem with Traditional Component Libraries In most Vue component libraries, styling logic lives inside the .vue file \u2014 in scoped CSS, computed class strings, or inline styles. That creates three recurring problems: Untestable styles \u2014 unit testing a block [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":224,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[5],"tags":[],"class_list":["post-15","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-advanced-web-app-with-nuxt-and-net"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v27.7 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>Building a Headless Design System in Vue 3 \u2014 The Compose Pattern - Software Production<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/softwareproduction.eu\/?p=15\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Building a Headless Design System in Vue 3 \u2014 The Compose Pattern - Software Production\" \/>\n<meta property=\"og:description\" content=\"Eighth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. The Problem with Traditional Component Libraries In most Vue component libraries, styling logic lives inside the .vue file \u2014 in scoped CSS, computed class strings, or inline styles. That creates three recurring problems: Untestable styles \u2014 unit testing a block [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/softwareproduction.eu\/?p=15\" \/>\n<meta property=\"og:site_name\" content=\"Software Production\" \/>\n<meta property=\"article:published_time\" content=\"2026-06-06T21:48:30+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2026-06-07T01:21:52+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/08-headless-design-system-compose-pattern.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1880\" \/>\n\t<meta property=\"og:image:height\" content=\"1055\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"Munir Husseini\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Munir Husseini\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"8 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=15#article\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=15\"},\"author\":{\"name\":\"Munir Husseini\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\"},\"headline\":\"Building a Headless Design System in Vue 3 \u2014 The Compose Pattern\",\"datePublished\":\"2026-06-06T21:48:30+00:00\",\"dateModified\":\"2026-06-07T01:21:52+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=15\"},\"wordCount\":793,\"commentCount\":0,\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=15#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/08-headless-design-system-compose-pattern.jpg\",\"articleSection\":[\"Advanced Web App with Nuxt and .NET\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=15#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=15\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=15\",\"name\":\"Building a Headless Design System in Vue 3 \u2014 The Compose Pattern - Software Production\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=15#primaryimage\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=15#primaryimage\"},\"thumbnailUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/08-headless-design-system-compose-pattern.jpg\",\"datePublished\":\"2026-06-06T21:48:30+00:00\",\"dateModified\":\"2026-06-07T01:21:52+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=15#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/softwareproduction.eu\\\/?p=15\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=15#primaryimage\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/08-headless-design-system-compose-pattern.jpg\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/06\\\/08-headless-design-system-compose-pattern.jpg\",\"width\":1880,\"height\":1055},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/?p=15#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/softwareproduction.eu\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Building a Headless Design System in Vue 3 \u2014 The Compose Pattern\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#website\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"name\":\"Softwareproduction\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/softwareproduction.eu\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#organization\",\"name\":\"Munir Husseini\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"contentUrl\":\"https:\\\/\\\/softwareproduction.eu\\\/wordpress\\\/wp-content\\\/uploads\\\/2026\\\/05\\\/softwareproduction-logo-32.png\",\"width\":32,\"height\":32,\"caption\":\"Munir Husseini\"},\"image\":{\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/logo\\\/image\\\/\"}},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/softwareproduction.eu\\\/#\\\/schema\\\/person\\\/fec48f54713e1bd117640fb9b748802f\",\"name\":\"Munir Husseini\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"contentUrl\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g\",\"caption\":\"Munir Husseini\"},\"sameAs\":[\"https:\\\/\\\/softwareproduction.eu\\\/\"],\"url\":\"https:\\\/\\\/softwareproduction.eu\\\/?author=1\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Building a Headless Design System in Vue 3 \u2014 The Compose Pattern - Software Production","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/softwareproduction.eu\/?p=15","og_locale":"en_US","og_type":"article","og_title":"Building a Headless Design System in Vue 3 \u2014 The Compose Pattern - Software Production","og_description":"Eighth in a series about migrating from legacy architectures to a modern Nuxt 4 stack. The Problem with Traditional Component Libraries In most Vue component libraries, styling logic lives inside the .vue file \u2014 in scoped CSS, computed class strings, or inline styles. That creates three recurring problems: Untestable styles \u2014 unit testing a block [&hellip;]","og_url":"https:\/\/softwareproduction.eu\/?p=15","og_site_name":"Software Production","article_published_time":"2026-06-06T21:48:30+00:00","article_modified_time":"2026-06-07T01:21:52+00:00","og_image":[{"width":1880,"height":1055,"url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/08-headless-design-system-compose-pattern.jpg","type":"image\/jpeg"}],"author":"Munir Husseini","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Munir Husseini","Est. reading time":"8 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/softwareproduction.eu\/?p=15#article","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/?p=15"},"author":{"name":"Munir Husseini","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f"},"headline":"Building a Headless Design System in Vue 3 \u2014 The Compose Pattern","datePublished":"2026-06-06T21:48:30+00:00","dateModified":"2026-06-07T01:21:52+00:00","mainEntityOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=15"},"wordCount":793,"commentCount":0,"publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=15#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/08-headless-design-system-compose-pattern.jpg","articleSection":["Advanced Web App with Nuxt and .NET"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/softwareproduction.eu\/?p=15#respond"]}]},{"@type":"WebPage","@id":"https:\/\/softwareproduction.eu\/?p=15","url":"https:\/\/softwareproduction.eu\/?p=15","name":"Building a Headless Design System in Vue 3 \u2014 The Compose Pattern - Software Production","isPartOf":{"@id":"https:\/\/softwareproduction.eu\/#website"},"primaryImageOfPage":{"@id":"https:\/\/softwareproduction.eu\/?p=15#primaryimage"},"image":{"@id":"https:\/\/softwareproduction.eu\/?p=15#primaryimage"},"thumbnailUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/08-headless-design-system-compose-pattern.jpg","datePublished":"2026-06-06T21:48:30+00:00","dateModified":"2026-06-07T01:21:52+00:00","breadcrumb":{"@id":"https:\/\/softwareproduction.eu\/?p=15#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/softwareproduction.eu\/?p=15"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/?p=15#primaryimage","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/08-headless-design-system-compose-pattern.jpg","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/08-headless-design-system-compose-pattern.jpg","width":1880,"height":1055},{"@type":"BreadcrumbList","@id":"https:\/\/softwareproduction.eu\/?p=15#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/softwareproduction.eu\/"},{"@type":"ListItem","position":2,"name":"Building a Headless Design System in Vue 3 \u2014 The Compose Pattern"}]},{"@type":"WebSite","@id":"https:\/\/softwareproduction.eu\/#website","url":"https:\/\/softwareproduction.eu\/","name":"Softwareproduction","description":"","publisher":{"@id":"https:\/\/softwareproduction.eu\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/softwareproduction.eu\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/softwareproduction.eu\/#organization","name":"Munir Husseini","url":"https:\/\/softwareproduction.eu\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/","url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","contentUrl":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/05\/softwareproduction-logo-32.png","width":32,"height":32,"caption":"Munir Husseini"},"image":{"@id":"https:\/\/softwareproduction.eu\/#\/schema\/logo\/image\/"}},{"@type":"Person","@id":"https:\/\/softwareproduction.eu\/#\/schema\/person\/fec48f54713e1bd117640fb9b748802f","name":"Munir Husseini","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","url":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/b07845732d4d7bddfc43e608ae6662d564a14b35706dfae0c9610071d978f54e?s=96&d=mm&r=g","caption":"Munir Husseini"},"sameAs":["https:\/\/softwareproduction.eu\/"],"url":"https:\/\/softwareproduction.eu\/?author=1"}]}},"jetpack_featured_media_url":"https:\/\/softwareproduction.eu\/wordpress\/wp-content\/uploads\/2026\/06\/08-headless-design-system-compose-pattern.jpg","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/15","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=15"}],"version-history":[{"count":10,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/15\/revisions"}],"predecessor-version":[{"id":225,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/posts\/15\/revisions\/225"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=\/wp\/v2\/media\/224"}],"wp:attachment":[{"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=15"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=15"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/softwareproduction.eu\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=15"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}