My simple, no-build Shopify theme workflow (Tailwind included)

I've developed 100's of custom Shopify themes over the 10+ years that I've spent working as a Shopify theme development specialist. When I first started working with themes, they were in their infancy, they were very simple and there was no need for a build process. As the years went by, things changed and as codebases started to get more complex, build processes within theme development started to become more common to handle tasks like JS transpilation, code minification etc.

Everyone has their own build process, I've contracted at many agencies and they all have a slightly different setup. Some use an old school Grunt/Gulp setup, some use NPM scripts and some use Vite/Parcel or similar. Sometimes these build processes work with no issues but I can't count the amount of hours I've seen wasted due to build process problems, usually down to dependency issues, e.g. having the wrong version of Node/NPM installed.

I remember when Shopify released Slate, it came with a complex build process which tried to do way too many things in my opinion. There was a lot of frustration from developers having issues with it and the project eventually got deprecated. Too many moving parts = more things that can potentially break. Since then, Shopify has released Dawn and then Horizon as their flagship starter themes which both use a "no-build" approach, there's less to go wrong without a build process and no time wasted trying to debug related issues due to a package discrepancy or operating system inconsistency.

I've experimented with all kinds of build processes over the years, I settled with a very lightweight NPM scripts based approach for a while and was quite happy with it. In recent times though, I've started to favour not using a build process at all. I kicked off a new full build theme project without a build process and I wanted to see how far I got until I caved, now I never want to touch a build process ever again!

In my previous, simple NPM scripts based build process, it handled a few things for me:

  • Javascript compilation/transpilation via Babel, this read my source JS files and outputted minified bundle JS files to serve to the browser
  • SVG icons, this went through a folder of SVG files and outputted an SVG icon sprite
  • Tailwind compilation, this read my source Tailwind CSS file and outputted a minified CSS file, using the JIT compiler so it only included classes used within the theme HTML

Javascript

To cut out the JS compilation step, I adopted a similar approach to Dawn/Horizon, just use separate JS files served directly to the browser with web components - this was considered bad practice for a while as it hindered page speed performance. This is no longer the case with modern HTTP/2 as it's multiplexed, meaning it can request hundreds of resources in parallel using a single TCP connection. Shopify also automatically minified these JS files when served in the browser, this gives us the best of both worlds, great developer experience with all the performance gains of using a bundler.

Icons

For SVG icons, I stopped using the sprite approach and leveraged the new(ish) inline_asset_content Liquid filter. Each SVG icon is a separate file within the theme "assets" directory and to output the icon, simply do:

{{ 'icon.svg' | inline_asset_content }}

Tailwind

The Tailwind compilation was the trickiest part of going "no-build", Tailwind has been my go-to solution for scalable CSS for years now and is it's usage is non-negotable for me so I had to find a decent solution for this. It turns out that Tailwind has a feature they call the Play CDN, this enables you to use the compiler directly inside of the browser without a build step. It's not meant for production use, it's a Javascript-based solution which dynamically generates CSS based on the page HTML and is meant to be used to test Tailwind features and to build prototypes.

You can also provide the Tailwind Play CDN with config, I assumed it'd eventually break with a complex config setup with many colors, font sizes, variants etc. but it's very stable in my experience. To get started with this approach, I added a new file /assets/tailwind.css and populated it with a simple config:

@import "tailwindcss";

@theme {
  --color-primary: red;
  --color-secondary: green;
  --color-tertiary: blue;
}

Within my /layout/theme.liquid file, I added this snippet:

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

<style type="text/tailwindcss">
  {{ 'tailwind.css' | inline_asset_content }}
</style>

That's it, we now have full use of Tailwind during theme development without the need for a build process. See the below demo video of this in action, notice how new classes are automatically generated as the HTML changes, the Play CDN uses a mutation observer to detect HTML changes and the Tailwind JIT compiler dynamically generates new classes on the fly and injects the necessary CSS into the DOM, it makes for very a very smooth developer experience.

This is a great workflow during development but what about production? This is where it gets a bit more complex, I started off by adding this snippet to my /config/settings_schema.json file:

{
  "name": "Mode",
  "settings": [
    {
      "type": "select",
      "id": "mode",
      "label": "Mode",
      "options": [
        {
          "value": "production",
          "label": "Production"
        },
        {
          "value": "production-css-inline",
          "label": "Production (CSS inline)"
        },
        {
          "value": "development",
          "label": "Development"
        }
      ]
    }
  ]
}

Then within my /layout/theme.liquid file, I changed my previously added CSS snippet to this:

{% case settings.mode %}
  {% when 'production' %}
    <link rel="stylesheet" href="{{ 'tailwind.min.css' | asset_url }}">
  {% when 'production-css-inline' %}
    <style>
      {{ 'tailwind.min.css' | inline_asset_content }}
    </style>
  {% when 'development' %}
    <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

    <style type="text/tailwindcss">
      {{ 'tailwind.css' | inline_asset_content }}
    </style>
{% endcase %}

This adds the following section to the global theme settings within the theme customiser:

This "Mode" setting has three options: "Production", "Production (CSS inline)" and "Development". if set to "Production", the theme will use a regular CSS file reference and look for the file /assets/tailwind.min.css. If set to "Production (CSS inline)", the theme will output the contents of the file /assets/tailwind.min.css within a <style></style> tag (this can be beneficial for performance, but only if the file is small enough, be careful with this and run performance tests if unsure). If set to "Development", the theme will use the Tailwind Play CDN as discussed above.

Build process and chill?

The only problem we have here is how do we go about using the /assets/tailwind.min.css file if it doesn't exist? This part does technically need a build process unfortunately, in theory this can be automated via a Git pre-commit hook and/or GitHub Action but I haven't looked into this yet. I handle this via a simple NPM script within my `package.json` file:

{
  "name": "shopify-theme-no-build",
  "scripts": {
    "build": "npx @tailwindcss/cli -i ./assets/tailwind.css -o ./assets/tailwind.min.css --minify"
  },
  "dependencies": {
    "@tailwindcss/cli": "^4.1.13",
    "tailwindcss": "^4.1.13"
  }
}

I simply just run npm run build before I push to production and that's it. I know this sounds like it defeats the purpose of not using a build process but the main thing I wanted to avoid was needing to use a build process during active development. When it comes to making changes to a project with this setup, it's just a case of pulling down the repo, running shopify theme dev, changing "Mode" to "Development" within the theme settings and I'm ready to go.

I've added a very bare-bones example of this setup within a Shopify theme in a GitHub repository tomblanchard/shopify-theme-no-build if you'd like to nerd-out further - enjoy and thanks for reading! ✌️