Mathias Polligkeit
  • Dev
  • Impro
  • Sheet Music
  • Contact
Nov 22, 2021 (last updated: Dec 7, 2021)

Design Systems, Pt. 3: Generating Custom Properties and Accessor Functions

In part 1 we defined the base variables for the system, in part 2 we generated the font size, line height and spacer values. Now it’s time to generate custom properties and add accessor functions.

Custom properties

Instead of using the SASS variables directly, we’re going to generate CSS custom properties from them. This will make it easier to support themes, without the need to recompile all the styles with a different set of variables.

@mixin define-custom-properties($theme) {
  $maps: (
    color: colors,
    font-family: font-families,
    size: sizes,
    lines: lines,
    letter-spacing: letter-spacings,
    weight: weights,
    radius: radii,
    border-width: border-widths
  );
  $single-values: (base-size, measure, gutter);

  @each $name, $key in $maps {
    @include map-to-properties($name, map.get($theme, $key));
  }

  @each $name in $single-values {
    --#{"" + $name}: #{map.get($theme, $name)};
  }
}

@mixin map-to-properties($name, $map) {
  @each $key, $value in $map {
    --#{"" + $name}-#{"" + $key}: #{$value};
  }
}

This iterates over all the map variables and defines one custom property for each item in the map. Instead of defining the variables on the :root pseudo class, you could add them to a .theme-my-theme class instead.

Accessor function

You could stop here and use the generated custom properties in your styles directly, but there is one shortcoming: SASS will not warn you if you are referencing a custom property that does not exist. And while you can you can access items from a SASS map with map.get($variable, key), this doesn’t read very nicely. Let’s define accessor functions instead.

To access values from the $theme variable, we’ll add this function:

@function theme($key) {
  @return map.get($theme, $key);
}

The accessor function for colors looks like this:

@function color($id) {
  @if not map.has-key(theme(colors), $id) {
    @error "Invalid color. Allowed values: #{map.keys(theme(colors))}.";
  }

  @return var(--color-#{"" + $id});
}

Which can be used in your styles like this:

.some-class {
  background: color(primary);
}

As you can see, this function uses var to reference the custom property belonging to the given key. This means you can easily support multiple themes just by overriding the custom properties.

The function also checks whether the given key exists, and if not, it prints the available keys, which saves you from digging into the variable definitions in case you misspell something.

Functions for font-family, size, letter-spacing, weight, radius, border-width and breakpoint can be defined accordingly.

Depending on whether you want to restrict the factors for spacers, you can either add the same kind of function, or you can add a simpler function that doesn’t validate the factor:

@function spacer($factor) {
  @return $factor * theme(spacer-base);
}

The lines function is slightly different, since the map we built uses the string representation of the fractions as keys, and I prefer to use decimal numbers in the styles. So we’ll use the original line-heights list for validation.

@function lines($factor) {
  @if not list.index(theme(line-heights), $factor) {
    @error "Invalid line height #{$lines}. Allowed values: #{theme(line-heights)}.";
  }

  $key: number_to_fraction($factor);
  @return var(--lines-#{"" + $key});
}

To add a 1.5 lines margin, you can now write:

.some-class {
  margin-bottom: lines(1.5);
}

Next part: Design Systems, Pt. 4: Generating Utility Classes

  • design-systems
  • sass

See Also

  • Design Systems, Pt. 9: Conclusion
  • Design Systems, Pt. 8: Layout Rules
  • Design Systems, Pt. 7: Project Structure
  • Design Systems, Pt. 6: Variable Export
  • Design Systems, Pt. 5: Themes
  • privacy policy