Initialize Sass variables from Hugo templates

Hugo v0.109.0 introduces an intuitive and convenient way to initialize Sass variables from your templates.

On this page:

  1. The old way
  2. The new way
  3. Usage notes
  4. Try it

The old way

With v0.108.0 and earlier, you assign template values to Sass variables using the resources.ExecuteAsTemplate method. When using this approach you must place Hugo template code at the top of your root Sass file, typically assets/scss/main.scss. Depending on the complexity of your project, this approach can be:

  • Aesthetically displeasing
  • Unwieldy or difficult to maintain
  • Contrary to the separation of concerns principle

A simplistic example using the (deprecated) LibSass transpiler:

config.toml

[params.style]
container_max_width = '768px'
font_family_base = 'sans-serif'
font_size_base = '18px'

structure

assets/scss/
├── _layout.scss
├── _typography.scss 
└── main.scss

main.scss

// Variables initialized with resources.ExecuteAsTemplate
$container-max-width: {{ site.Params.style.container_max_width }};
$font-family-base: {{ site.Params.style.font_family_base }};
$font-size-base: {{ site.Params.style.font_size_base }};

// Imports
@import "layout";
@import "typography";

baseof.html

{{ $options := dict
  "targetPath" "css/style.css"
  "transpiler" "libsass"
}}

{{ with resources.Get "scss/main.scss" | resources.ExecuteAsTemplate "" . | toCSS $options}}
  <link rel="stylesheet" href="{{ .RelPermalink }}">
{{ end }}

The new way

With v0.109.0 and later, Hugo injects a Sass module (hugo:vars) to initialize your Sass variables. Your Sass files will contain Sass rules, and nothing else.

Using the previous example with the Dart Sass transpiler:

config.toml (same as previous example)

[params.style]
container_max_width = '768px'
font_family_base = 'sans-serif'
font_size_base = '18px'

structure (same as previous example)

assets/scss/
├── _layout.scss
├── _typography.scss 
└── main.scss

main.scss

@use "layout";
@use "typography";

_layout.scss

@use "hugo:vars" as h;

.container {
  max-width: h.$container-max-width;
}

_typography.scss

@use "hugo:vars" as h;

body {
  font-family: h.$font-family-base;
  font-size: h.$font-size-base;
}

baseof.html

{{ $options := dict
  "targetPath" "css/style.css"
  "transpiler" "dartsass"
  "vars" site.Params.style
}}

{{ with resources.Get "scss/main.scss" | toCSS $options }}
  <link rel="stylesheet" href="{{ .RelPermalink }}">
{{ end }}

In the code above, look at the last key/value pair in the $options map:

"vars" site.Params.style

That’s where the magic is.

Usage notes

Key names

You can assign any non-nested map to the vars key in the $options map. Use snake_case to define the keys. For example:

{{ $v := dict 
  "font_size" "18px"
  "font_color" "#222",
}}

With this Sass rule:

@use "hugo:vars" as h;

You can access the Sass variables with:

h.$font-size
h.$font-color

Note that Sass variables, like all Sass identifiers, treat hyphens and underscores as identical. That means you can also access the Sass variables with underscores:

h.$font_size
h.$font_color

Do not use kebab-case to define the keys. Go identifiers may not contain hyphens. If the key name includes a hyphen, you cannot access the value with chained identifiers. For example, site.Params.style.font-size will throw a parsing error.

Do not use camelCase or PascalCase to define the keys. Internally, Hugo transforms all keys to lowercase. If you define a key named bodyBackgroundColor, the corresponding Sass variable will be $bodybackgroundcolor. It works, but is difficult to read.

LibSass transpiler

You can also use the built-in LibSass transpiler. Although LibSass does not support modules, the syntax is similar.

Using the previous example with the LibSass transpiler:

config.toml (same as previous example)

[params.style]
container_max_width = '768px'
font_family_base = 'sans-serif'
font_size_base = '18px'

structure (same as previous example)

assets/scss/
├── _layout.scss
├── _typography.scss 
└── main.scss

main.scss

@import "hugo:vars";
@import "layout";
@import "typography";

_layout.scss

.container {
  max-width: $container-max-width;
}

_typography.scss

body {
  font-family: $font-family-base;
  font-size: $font-size-base;
}

baseof.html

{{ $options := dict
  "targetPath" "css/style.css"
  "transpiler" "libsass"
  "vars" site.Params.style
}}

{{ with resources.Get "scss/main.scss" | toCSS $options }}
  <link rel="stylesheet" href="{{ .RelPermalink }}">
{{ end }}

Try it

The previous examples are simplistic. This example is more realistic, including:

You must install the Dart Sass transpiler before building this site.

After you have installed the Dart Sass transpiler:

git clone --single-branch -b hugo-github-issue-10558 https://github.com/jmooring/hugo-testing hugo-github-issue-10558
cd hugo-github-issue-10558
npm ci
hugo server

Files of interest:

  • layouts/_default/baseof.html
  • layouts/partials/css.html (called by baseof.html)
  • layouts/partials/get-scss-vars.html (called by css.html)
  • assets/scss/*
12 Likes

Using the old way, with main.scss, as in your example, given correct usage of resources.ExecuteAsTemplate and correct naming of .scss files, as well as $.Params in any cases where that is necessary, what is the best way to ensure consistent and correct operation using the following guards?

{{ with .Params.layout_one -}}
@import "layout_a";
{{- else -}}
@import "layout_b";
{{- end -}}

{{ if .Params.layout_c -}}
@import "layout_c";
{{- end -}}

Have you tried it yourself?

It didn’t used to work at all, if I’m not mistaken

Now it seems to work, but it’s challenging to test and confirm. I think the scss needs to recompile, i.e. ‘watch’ doesn’t work.

Killing the server and running it again ‘hugo server’ works in some cases. ‘hugo server --disableFastRender’ seems the better option.

Having said that, where main.scss lives in assets/scss/ and _test.scss lives in assets/scss/includes/test/, and test includes, for example:

$body_background: #2F4F4F;

body {
background:$body_background !important;
}

the following code in main.scss seems always to change the background red, with or without .Params.dark_mode = "true"

{{ with .Params.dark_mode  -}}
@import "/includes/test/test";
{{- else  }}
body {
background:#ff0000 !important;
}
{{- end }}

In my case, something along these lines, with @includes wrapped in switches, is preferable, but it seems unworkable, in terms of dev flow.

Something between disableFastRender and setting the asset cache to minimal maybe? Or a script in package.json to kill and restart the server?