@danske/sapphire-design-tokens
v41.1.0
Published
Design Tokens for the Sapphire Design System from Danske Bank A/S
Downloads
307
Readme
@danske/sapphire-design-tokens
Design tokens are atomic values like colors, sizes, border radius, transition descriptions etc. These are sometimes also called atoms, style properties, design variables, design constants. Each token represents a design decision.
We maintain these values as JSON and generate assets in different formats to be used in implementations of Sapphire (eg. css for web, json for iOS etc.). To achieve this we're using Style Dictionary.
Supported formats
- CSS variables
- javascript/es6 with TypeScript type declarations
Token types
A design token can be either global
or semantic
. You know a token type by
its prefix which is one of these words, but this depends on the format. For
example, in css variables we also add a sapphire
prefix because of the global
nature of css variables.
Global
Global tokens are the primitive values in our design language. The color palette, animation, typography, and dimension values are all recorded as global tokens. A global token is just a plain, context-agnostic name describing a value.
Examples:
CSS variables
--sapphire-global-color-dark-blue-100
--sapphire-global-size-generic-25
JS/TS
globalColorDarkBlue100
globalSizeGeneric25
Semantic
Semantic tokens relate to a specific context or abstraction. These help communicate the intended purpose of a global token. These are useful when a global token (ie. dark blue) will appear in multiple places (eg. button background color, link color etc.).
The value of semantic tokens should always be a reference to a global token.
Examples:
CSS variables
--sapphire-semantic-color-warning
--sapphire-semantic-size-radius-small
JS/TS
semanticColorTextPrimary
semanticSizeRadiusSmall
Development
Tokens are found in tokens/
. All JSON
files in this directory (and its
subdirectories) will be
processed by style-dictionary
.
Each generated design tokens format is described in
config.json
. And each format is generated by a pipeline of
transforms
which can change both the name of each design token and the format
of the value. Read more about transforms here.
style-dictionary
recommends a certain
structure
to the JSON
describing the tokens. Some
transforms assume that
structure.
We don't want to be restricted by style-dictionary
's recommendation as it does
not fully meet our needs. Therefore we wrote out little
transform to allow us to nest things as we need them in our
JSON
files and still be compatible to what some of the built-in transforms
expect.
JSON structure
Each JSON
file should contain a root property which is one of global
if it
is a global token in tokens/global/
, [semantic
] for
semantic tokens in tokens/semantic/
.
The value of this property should be any object that follows
style-dictionary
's
recommendation.
Though the most important are the first two layers there.
Aliasing
You can reference (alias) existing values by using the dot-notation object path (the fully articulated property name) in brackets. Note that this only applies to values; referencing a non-value property will cause unexpected results in your output.
Examples
tokens/global/colors.json
{
"global": {
"color": {
"red": {
"100": {
"value": "hsl(355, 74.7%, 49.6%)"
},
"transparent": {
"value": "hsla(353, 71%, 52%, 0.65)"
}
}
}
}
}
tokens/semantic/colors.json
{
"semantic": {
"color": {
"background": {
"action": {
"primary": {
"default": {
"value": "{global.color.action.value}"
},
"hover": {
"value": "{global.color.cyan.90.value}"
},
"focus": {
"value": "{global.color.cyan.100.value}"
},
"down": {
"value": "{global.color.cyan.100.value}"
},
"disabled": {
"value": "{global.color.cyan.80.value}"
}
}
}
}
}
}
}
Responsive Tokens
A token can be defined to hold different values based on the breakpoints
defined in the theme. Use the responsiveValue
property as a
sibling to the usual value
which all tokens have.
The responsiveValue
property must be an object where the keys are the
breakpoint names and the values are the token's value for that
breakpoint.
Each output format would deal with these responsive values differently.
In the css
output: This will generate media queries which change the value of
the css variable to the one defined for each size. References to other tokens
are preserved.
In the ts-theme
object output: This will have no effect, because the theme
object only refers to the name of the css variable, not its value (which could
change based on media queries).
In the js
and json
formats: The value of a token with responsiveValue
s
will be an object which is a map from breakpoint name to the resolved value.
This object will always have a base
key which is the default value.
Examples
{
"button": {
"size": {
"height": {
"value": "{semantic.size.height.control.default.value}",
"responsiveValue": {
"s": "100", // you can also use hardcoded values, not only references
"l": "{semantic.size.height.control.sm.value}"
"m": "{semantic.size.height.control.lg.value}",
}
}
}
}
}
Breakpoints
The name and values of a theme's breakpoints should be defined as semantic
tokens at semantic.size.breakpoint
.
The keys in the object are the names of each breakpoint, which can be referenced in responsive tokens, and the values are pixel sizes representing the min width of the screen size.
Remember that these breakpoints are just regular tokens and they can be consumed like all other tokens are in each output.
Note on Media Queries
It is widely considered good practice to design with the smallest screen in view and define what happens as the screen size grows.
In CSS media queries this translates in the following principle: a css value outside a media query describes the UI from the smallest screen size and media queries targeting screen sizes based on "min-width" will overwrite the value as the screen size grows.
This means that if you want a token that has one value for mobile and one for large desktops you would do something like this:
{
"button": {
"size": {
"height": {
"value": "some-value-for-the-smallest-screens",
"responsiveValue": {
"m": "some-value-for-medium-screens-and-up"
}
}
}
}
}
Building
The build command (npm run build
) consists of two parts:
build:tokens
for generating tokens in different output formatsbuild:components
for building the complimentary react components for showing tokens list.
Node.js v>12 is required for building.
Themes
This package can build multiple sets of tokens for each platform and format. One can add more tokens or overwrite a subset of the default tokens to create a completely new set of token exports. This is what it means to have a new theme in Sapphire.
The folder tokens/default/
contains the default theme, which is also the theme
which all other themes can modify.
Therefore, a theme in @danske/sapphire-design-tokens
is a standalone and complete
set of tokens. Each token could have a value different than the same token in the
default theme, but not necessarily. In fact, we forsee that most, if not all
themes would only overwrite a minority of tokens from the default theme.
Creating a new theme
- Create a new folder in
tokens/
with your theme's name which itself contains one or all three folders for each layers of tokens. The pattern is easy to observe intokens/default/
. Example:tokens/dark/global
,tokens/dark/semantic
- Change the value of some of the tokens which you can find in
tokens/default/
by creating a token in your theme with the same name but different value. It's a good idea to do this in json files that have the same name as those it overwrites from the default theme. - Add any assets specific to your theme in
assets/
. - Add your theme's source, assets and other details in
themes.json
. Thesource
should refer to those json files with tokens specific to your theme. ThebasedOnSource
should refer to those json files with tokens which the new theme extends/overwrites. The difference betweensource
andbasedOnSource
is that if insource
you overwrite a token frombasedOnSource
it won't be a build error. Theassets
are just copied over in the theme's build output. Example:{ name: 'default-dark', source: ['tokens/default-dark/**/*.json'], basedOnSource: ['tokens/default/**/*.json'], assets: ['assets/fonts/'], cssClassName: 'sapphire-theme-default-dark', outputFolderName: 'default-dark' }
- Build the tokens as you normally would and check the output in
build/themes/<your_theme-s_folder>/
Contrast themes
A theme can optionally be set up to be the "contrast" theme of another in
themes.json
. This will have an effect on the css outputted for that theme.
More specifically, we generate a new selector defining the tokens for the
contrast theme.
In practice, this means that the class name sapphire-theme-contrast
can be
used inside the DOM tree wrapped by the theme's class name. Everything under
sapphire-theme-contrast
will look like the contrast theme.
Example
The normal CSS output of a theme looks roughly like this:
.sapphire-theme-foo {
// ... css variables for "foo" theme are defined here
}
If the foo
theme is defined as the contrast theme of theme bar
in
themes.json
it will instead generate something like this:
.sapphire-theme-foo {
// ... css variables for "foo" theme are defined here
}
.sapphire-theme-foo .sapphire-theme-contrast {
// ... css variables for "bar" theme are defined here
}
Which implies that you can simply import a theme and use its contrast from the same import without needing to know which pairs of contrast themes exist. And as a consequence, if you change the parent theme, the contrast would follow as well.
Caveats
There are a few caveats with this, but at the moment they don't hinder us:
- If a pair of contrasting themes have different assets (fonts, images etc.) on which their respective tokens depend on, it will not work.
- If a pair of contrasting themes have different responsive tokens they won't work.