Encapsulating Markup and Styles in Shopify with CodeKit's Hooks and SCSS

Before React, Vue, and other reactive JavaScript frameworks took over the front end development world, web developers practically lived by a single axiom based on the notion of separation of concerns:

Separate the content, markup, and styles.

There were many seemingly important reasons for this separation which was treated as canon in the web development world. Among them:

  • separation is necessary for readability and maintainability
  • separation leads to less code duplication
  • separating CSS made styles cacheable by the browser
  • separation allows for the same content to be styled in multiple ways

Then React and JSX came along and crushed the axiom of separation of concerns in one fell swoop. In essence, what JavaScript frameworks—along with many of the style tools like CSS-In-JS—are saying is:

Separation of concerns doesn't matter. It all ends up in the browser anyway.

In fact, one of the prime motivators for all of the JS framework CSS tooling was to scope styles to their components. Svelte.js handles this out-of-the-box which is really nice.

Don't hate, encapsulate

After working with React and Svelte it made much more sense to me to keep my styles scoped to the components or files that need them rather then spread out across modules or breakpoints. Keeping everything encapsulated is actually easier to maintain than modular SCSS. And, it comes with the added benefit of each file or component only loading the styles it needs.

Sure, I could write regular ol' css in <style></style> tags at the top of each .liquid file but then I can't use all the goodies of SCSS. After you get used to nesting properties in SCSS, it is really hard to go back to CSS.

With that in mind, I wanted all the benefits of .scss: variables, nesting, mixins, yet the styles are scoped to the file that needs it. In other words, encapsulated with the file.

What if I could write styles in .scss but process it and append the processed css to a matching .liquid file in <style></style> tags at the top of the file?

It's so crazy, it just might work!

But how to accomplish this feat? I never got into Gulp or Grunt but I would assume you could build something like that into your workflow with those tools.

But then it hit me: I've been using CodeKit for years with WordPress and absolutely love it. CodeKit's Hooks feature might be just what I need. With a custom hook, maybe a bash script could take the outputted .css file and write it to a .liquid file.

I'm not a bash or shell scripting wizard by any means but hobbled something together that actually worked in about an hour. Needless to say I was pretty pleased. Read on to find out how I did it.

Prerequisites

For the purposes of this solution, I am using Quickshot which is a great Shopify local development tool but this will work with ThemeKit or Slate as well.

The first thing you will want to do is create a /styles directory in your local Shopify theme. Shopify will ignore anything that is not in a default theme directory. Your directory structure should look like this (notice the styles directory after snippets:

.
└── theme
    ├── assets
    ├── config
    ├── layout
    ├── locales
    ├── sections
    ├── snippets
    ├── styles
    └── templates

The basic pattern is, for every .liquid file you want to append styles to, you will create a corresponding .scss file with the same name. CodeKit will output a .css file with this filename as well which is important as you will see later.

For example, if you have a liquid file product-template.liquidyou would create a new scss file with the same name but with the .scss extension: product-template.scss in your /styles directory.

Again, keeping these files in the /styles directory ensures that they are ignored by Shopify which is what we want.

CodeKit Setup

Import your top-level theme directory into CodeKit. Once imported, go to the file list of your project in CodeKit. On all of the directories except /styles, right-click the directory and select 'Skip this folder'. You could also option-click multiple and then select 'Skip selected folders'. CodeKit will ignore these files which is what we want.

Then, in CodeKit open up the /styles directory and select the product-template.scss file you created above and set the output options on the right hand panel:


Compiler: Dart Sass [default]

Output Style: Compressed [important so it is all on one line]

Other Options:

  • deselect Create a source map
  • deselect Emit a @charset for non-ascii text

After Compiling:

  • Run Bless on the CSS file [optional]
  • select Emit a @charset for non-ascii text

Output

  • When This File Changes or Builds: Compile It
  • To This Path: /theme/styles/your-file.scss

Note #1: the default is to output to a /css directory but I like to keep all the styles together so I set this to the same directory for each file.

Note #2: you will need to set these options for every .scss file you add to your /styles directory each time unless you set these as the defaults in CodeKit.

Hook Setup

Here's the basic hook setup in CodeKit:

And here's the script:

# Bash script for CodeKit hook to append processed css 
# from scss file to liquid file.
#
# (make sure to use 'Compressed' for scss->css output 
# in CodeKit so everything is on one line)
#
# To use: 
# 1. create a `/styles` directory in your Shopify theme
# for your .scss files
# 2. name the .scss file the same name as the liquid 
# file; e.g. sample.scss/sample.liquid
# 3. IMPORTANT: add empty `<style></style>` tags to first line of liquid file; add empty line below
# 4. add this script to your project in CodeKit in Hooks
# 5. profit

## NOTES:
# $CK_OUTPUT_PATH is the processed css file
# (provided variable from CodeKit)

# $CK_PROJECT_ROOT is the project root folder
# (another provided variable from CodeKit)

# get contents of css file
css=$(<$CK_OUTPUT_PATH)

# get filename without extension; save to FILE variable
s=$CK_OUTPUT_PATH
s=${s##*/}
FILE=${s%.css}

# set up our <style> tags
S1='<style>'
S2='</style>'

# concat css with <style> tags for output
DATA=$S1$css$S2

# search through project to find liquid file with same name as file
STYLEFILE=$(find $CK_PROJECT_ROOT -type f -name "${FILE}.liquid")

# replace first line of .liquid file with processed css in <style> tags.
#w00t.
{ printf '%s\n' "$DATA"; sed '1d' $STYLEFILE; } > tmp.liquid && mv tmp.liquid $STYLEFILE


Let's go through this script in detail:

css=$(<$CK_OUTPUT_PATH)`

^^ Using the CodeKit-supplied CK_OUTPUT_PATH variable, we put the contents of the outputted css file into a variable, aptly named css.


s=$CK_OUTPUT_PATH
s=${s##*/}
FILE=${s%.css}

^^ Here we're getting the outputted file path (our .css file), then getting the file name without the extension. Why? Now we have the file name to look for to append our css to.


S1='<style>'
S2='</style>'

^^ Here we set up our style tags to wrap the css data in.


DATA=$S1$css$S2

^^ We concatenate our <style> and </style> tags with the actual css in the middle and save to a variable DATA. This is the data we are appending to the file.


STYLEFILE=$(find $CK_PROJECT_ROOT -type f -name "${FILE}.liquid")

^^ We recursively search through the project directory using the CodeKit-supplied CK_PROJECT_ROOT to find a file with the same name as our css file which is stored in the FILE variable; save this matching file to the STYLEFILE variable.


{ printf '%s\n' "$DATA"; sed '1d' $STYLEFILE; } > tmp.liquid && mv tmp.liquid $STYLEFILE

^^ Finally, we write our DATA to the first line of the matching file (STYLEFILE) by creating a temp file, then overwriting the original file.

And that's it! Your .liquid file now has scoped styles!

Here is some sample output from my collection-ma-header.scss to the collection-ma-header.liquid file:

If you're using Quickshot's theme watch --sync CLI command, whenever your .liquid file is updated with new styles, it will upload it to your synced theme.

Seeing this jumble of styles at the top of your file may seem a bit jarring at first but you get used to it pretty quick. Try it with one file and see if you like it. Now that I'm using this system, I'll never go back.

Extending

With a little re-jigging the script to use .php as the extension instead of .liquid, I've gotten this hook working in a WordPress project.

You could even make the script more global by adding an EXT variable to store the extension and replacing that where .liquid is in the script.

More

One VSCode extension that has really helped with this workflow is Working Sets which allows you to group files in sets of your choosing for mini sub-projects within a project. I'm using it in my Shopify development to keep my scoped .liquid and .scss files together.