{"id":6712,"date":"2025-08-11T11:18:06","date_gmt":"2025-08-11T16:18:06","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=6712"},"modified":"2025-08-17T22:59:35","modified_gmt":"2025-08-18T03:59:35","slug":"architecture-through-component-colocation","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/architecture-through-component-colocation\/","title":{"rendered":"A Nice Vanilla App Architecture Using Web Components and CSS Module Scripts"},"content":{"rendered":"\n<p>It&#8217;s an established Good Idea\u2122 that building digital interfaces of any kind is best done by building components and then piecing together the interfaces from those components. This can be sliced and diced a lot of ways, but generally: a component is a reasonable independent peice of what that interface needs. When it comes to websites, things like a header, footer, grid, card, button, etc. A design system, as it were. See concepts like <a href=\"https:\/\/atomicdesign.bradfrost.com\/\">Atomic Design<\/a>. <\/p>\n\n\n\n<p>A nice by-product of the Rise of JavaScript Frameworks is that they solidified this idea. React, Vue, Svelte&#8230; you work with them by building components and composing them together. That&#8217;s their point. <\/p>\n\n\n\n<p>I like the idea of userland tools like JavaScript frameworks pushing the boundaries, then the web evolving to not require those tools. <em>So can we pull off a component-structured project without any build process or framework?<\/em> We&#8217;re close. <\/p>\n\n\n\n<p>This is an example of how I&#8217;d like to structure a website: <\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"488\" height=\"462\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/Screenshot-2025-08-09-at-1.19.58-PM.png?resize=488%2C462&#038;ssl=1\" alt=\"\" class=\"wp-image-6722\" style=\"width:317px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/Screenshot-2025-08-09-at-1.19.58-PM.png?w=488&amp;ssl=1 488w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/Screenshot-2025-08-09-at-1.19.58-PM.png?resize=300%2C284&amp;ssl=1 300w\" sizes=\"auto, (max-width: 488px) 100vw, 488px\" \/><\/figure>\n\n\n\n<p>Those components (in our simple example, a button, card, and header) are all:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Inside a <code>components<\/code> folder, each with their own named folder (organized!)<\/li>\n\n\n\n<li>Have a file for their template and logic<\/li>\n\n\n\n<li>Have a separate CSS file<\/li>\n<\/ul>\n\n\n\n<p>So like this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"480\" height=\"546\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/Screenshot-2025-08-09-at-10.59.43-AM.png?resize=480%2C546&#038;ssl=1\" alt=\"\" class=\"wp-image-6717\" style=\"width:325px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/Screenshot-2025-08-09-at-10.59.43-AM.png?w=480&amp;ssl=1 480w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/Screenshot-2025-08-09-at-10.59.43-AM.png?resize=264%2C300&amp;ssl=1 264w\" sizes=\"auto, (max-width: 480px) 100vw, 480px\" \/><\/figure>\n\n\n\n<p>This is the kind of logical grouping and isolation that makes sense to me in creating a component architecture. A more complex setup might have components with, say, <code>.graphql<\/code> files, their own images, tests, etc. The co-location is key to sanity. <\/p>\n\n\n\n<p>Our components are JavaScript here because there is <a href=\"https:\/\/frontendmasters.com\/blog\/seeking-an-answer-why-cant-html-alone-do-includes\/\">no concept of HTML includes yet<\/a>, but also that <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/API\/Web_components\">web components<\/a> are a generally nice way to handle this anyway, and they require JavaScript instantiation. We don&#8217;t need any framework to use web components (hence &#8220;vanilla app architecture&#8221;), but in the demo, I&#8217;ll use <a href=\"https:\/\/lit.dev\/\">Lit<\/a> (just a light helper library). <\/p>\n\n\n\n<p>How do we integrate those <code>component.js<\/code> and <code>component.css<\/code> files? That question has long lingered for me. Bundlers can do this job. For instance, webpack just invented their own way of dealing with it. If you type <code>import \".\/card.css\";<\/code> in a JavaScript file that is processed by webpack, it&#8217;ll just know what you mean and ensure that CSS is loaded on the page somehow. Likewise, <a href=\"https:\/\/vite.dev\/guide\/features.html#css\">Vite just does it&#8217;s own thing<\/a>:<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p>Importing\u00a0<code>.css<\/code>\u00a0files will inject its content to the page via a\u00a0<code>&lt;style><\/code>\u00a0tag with HMR support.<\/p>\n<\/blockquote>\n\n\n\n<p>That&#8217;s great and all, but we&#8217;re trying to go vanilla here. No bundler\/build process. How do we import CSS like that? <\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Enter CSS Module Scripts<\/h2>\n\n\n\n<p><strong>Good news:<\/strong> JavaScript has an answer to that question we just asked, and it&#8217;s called <a href=\"https:\/\/web.dev\/articles\/css-module-scripts\">CSS Module Scripts<\/a>.<\/p>\n\n\n\n<p><strong>Bad news:<\/strong> Only Chrome supports it. (<a href=\"https:\/\/bugs.webkit.org\/show_bug.cgi?id=227967\">WebKit bug<\/a>; <a href=\"https:\/\/bugzilla.mozilla.org\/show_bug.cgi?id=1720570\">Firefox bug<\/a>)<\/p>\n\n\n\n<p>Google&#8217;s blog post on them (linked above) is <span style=\"box-sizing: border-box; margin: 0px; padding: 0px;\">one of the few pieces of information available about them, and it contains some\u00a0<em><strong>incorrect syntax<\/strong><\/em><\/span>, so be careful there. It should look like this (the <code>with<\/code> keyword is correct, if you see <code>assert<\/code> that&#8217;s old\/wrong):<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> sheet <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/styles.css'<\/span> <span class=\"hljs-keyword\">with<\/span> { <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">'css'<\/span> };<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>When you do that (in a supporting browser), <code>sheet<\/code> becomes a &#8220;Constructable Stylesheet&#8221; and then you can use it to, in our case, apply it to the Shadow Root of a web component.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">MyComponent<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">HTMLElement<\/span> <\/span>{\n  <span class=\"hljs-keyword\">constructor<\/span>() {\n    <span class=\"hljs-keyword\">super<\/span>(); \n    <span class=\"hljs-keyword\">const<\/span> shadowRoot = <span class=\"hljs-keyword\">this<\/span>.attachShadow({ <span class=\"hljs-attr\">mode<\/span>: <span class=\"hljs-string\">'open'<\/span> });\n    shadowRoot.adoptedStyleSheets = &#91;sheet];\n  }\n  \n  ...<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>These &#8220;import attributes,&#8221; as I think they are called, can do other things. It&#8217;s much better supported to import JSON this way, like:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> sheet <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/data.json'<\/span> <span class=\"hljs-keyword\">with<\/span> { <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">'json'<\/span> };<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h3 class=\"wp-block-heading\">Lit<\/h3>\n\n\n\n<p>Using <a href=\"https:\/\/lit.dev\/\">Lit<\/a>, applying the styleheet (or, &#8220;the constructable stylesheet, as imported via CSS module scripts&#8221; to do the whole mouthful) is like this:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">import<\/span> {html, LitElement} <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'lit'<\/span>;\n<span class=\"hljs-keyword\">import<\/span> sheet <span class=\"hljs-keyword\">from<\/span> <span class=\"hljs-string\">'.\/button.css'<\/span> <span class=\"hljs-keyword\">with<\/span> { <span class=\"hljs-attr\">type<\/span>: <span class=\"hljs-string\">'css'<\/span> };\n\n<span class=\"hljs-class\"><span class=\"hljs-keyword\">class<\/span> <span class=\"hljs-title\">My<\/span> <span class=\"hljs-title\">Component<\/span> <span class=\"hljs-keyword\">extends<\/span> <span class=\"hljs-title\">LitElement<\/span> <\/span>{\n  <span class=\"hljs-keyword\">static<\/span> styles = &#91;sheet];\n\n  ...<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<h2 class=\"wp-block-heading\">Demo<\/h2>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_XJmppKN\" src=\"\/\/codepen.io\/editor\/anon\/embed\/XJmppKN?height=450&amp;theme-id=1&amp;slug-hash=XJmppKN&amp;default-tab=js,result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed XJmppKN\" title=\"CodePen Embed XJmppKN\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n","protected":false},"excerpt":{"rendered":"<p>CSS module scripts help keep the dream of co-locating files that all relate to a component, without needing a bundler. <\/p>\n","protected":false},"author":1,"featured_media":6726,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[],"class_list":["post-6712","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/08\/containers.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6712","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=6712"}],"version-history":[{"count":13,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6712\/revisions"}],"predecessor-version":[{"id":6787,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/6712\/revisions\/6787"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/6726"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=6712"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=6712"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=6712"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}