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