Generating custom propreties from a configuration with Sass

It's possible to generate CSS using WordPress' theme.json.

Any values declared within the custom field will be transformed to CSS Custom Properties following this naming schema: --wp--custom--<variable-name>

If you have this in your JSON file:

{
    "version": 1,
    "settings": {
        "custom": {
            "line-height": {
                "body": 1.7,
                "heading": 1.3
            }
        }
    }
}

then this is going to be interpreted and outputted like this:

body {
    --wp--custom--line-height--body: 1.7;
    --wp--custom--line-height--heading: 1.3;
}

The Global Settings & Styles (theme.json) page goes in detail about this functionality.

Even if you don't buy into "writing your CSS with a JSON file", there's something "nice" about declaring the values using an object rather than having a bunch of custom properties. It just gives a bit more structure.

Currently, I see little benefit in generating the CSS from a JSON file for bespoke, enterprise-size websites. However, I welcome this feature for themes that are used by thousands and are open to customization.

But I can see myself writing an object (map) and letting a function or mixin generate the custom properties. I want a Sass code like this:

$config: (
    table: (
        caption: (
            color: gray
        ),
        footer: (
            typography: (
                font-weight: bold
            )
        )
    )
);

:root {
    @include map-to-custom-propreties($config);
}

to be outputted in the CSS like this:

:root {
  --table--caption--color: gray;
  --table--footer--typography--font-weight: bold;
}

The configuration doesn't have to be even a giant map; it can come from multiple places and be combined, like so:

@use 'sass:map';
@use 'table/custom-propreties' as table-custom-propreties;
@use 'quote/custom-propreties' as quote-custom-propreties;

:root {
    @include map-to-custom-properties(
        map.deep-merge(
            table-custom-propreties.$config,
            quote-custom-propreties.$config
        )
    );
}

Solution

Two things have to be solved for the imagined map-to-custom-propreties utility. One is traversing the configuration map, which should be as deep and with as many elements as desired. The other part is generating the custom property's name.

Sass doesn't have a function that is similar to the join (JavaScript) method or the implode (PHP). There are a couple of implementation out-there, here's one from danielpchen and one from Kitty.

The one I implemented looks like this:

@function _implode-list($list, $separator: '--') {
    $listLength: list.length($list);
    $result: '';

    @each $element in $list {
        $nextAdd: $element;

        @if list.index($list, $element) != $listLength {
            $nextAdd: string.insert($element, $separator, string.length($element) + 1);
        }

        $result: string.insert($result, $nextAdd, string.length($result) + 1);
    }

    @return $result;
}

It's general-purpose enough for my current needs and uses the new built-in modules.

The traversing part is the core of the main mixin. It's recursive to handle any complex map, and its purpose is to accumulate the name's pieces. It outputs the custom property using the previous internal function when it reaches the value definition.

@mixin map-to-custom-properties($config, $list: (), $separator: '--') {
    @each $key, $value in $config {
        @if meta.type-of($value) == 'map' {
            @include map-to-custom-properties($value, list.append($list, $key), $separator);
        } @else {
            --#{_implode-list(list.append($list, $key), $separator)}: #{$value};
        }
    }
}

With unit tests written for the function and mixin, I can see this become something that can be reused from project to project.