Style-dictionary: json to scss

Created on 2 Dec 2020  路  7Comments  路  Source: amzn/style-dictionary

I have listed here the issue I am trying to solve.

stackoverflow question

Convert following from

{"component": { "button": { "padding": { "value": "{size.padding.medium.value}" }, "font-size": { "value": 2 }, "text-align": { "value": "center" }, "primary": { "modifier": "true", "background-color": { "value": "hsl(10, 80, 50)" }, "color": { "value": "{color.font.inverse.value}" } }, "secondary": { "modifier": "true", "background-color": { "value": "{color.background.primary.value}" }, "color": { "value": "{color.font.link.value}" } } } } }

to

.component-button { padding: " "; font-size: " "; & .primary { background-color: "", color: "" } }

question

Most helpful comment

(I posted an answer to your Stack Overflow question but thought I鈥檇 cross-post it here too.)

The way I would do this is create a custom format and template. I would also move all of the tokens that you want to be transformed out of the component category and use pointers instead. I did this for the sample button.json token file used to generate the final file.

The files needed to run the example are below.

button.json

{
  "color": {
    "background": {
      "primary": { "value": "hsl(10, 80, 50)" },
      "secondary": { "value": "hsl(10, 80, 90)" }
    },
    "font": {
      "inverse": { "value": "#fff" },
      "link": { "value": "#00f" }
    }
  },
  "size": {
    "2": { "value": 16 },
    "padding": {
      "medium": { "value": 16 }
    }
  },
  "component": {
    "button": {
      "padding": { "value": "{size.padding.medium.value}" },
      "font-size": { "value": "{size.2.value}" },
      "text-align": { "value": "center" },
      "primary": {
        "background-color": { "value": "{color.background.primary.value}" },
        "color": { "value": "{color.font.inverse.value}" }
      },
      "secondary": {
        "background-color": { "value": "{color.background.secondary.value}" },
        "color": { "value": "{color.font.link.value}" }
      }
    }
  }
}

index.js

const _ = require('lodash')

/*
  This is formatted oddly in order to get a nice final shape
  in `button.scss`.

  Essentially what the template is doing is looping through
  the props object and outputting the top-level properties
  as the parent classnames, then for each child props of
  "classname" it looks to see if the child prop is an object,
  if it is, then it outputs the Sass `&.` operator with the
  child prop rule as the sub classname and then each child
  prop of the value as the final CSS style rule and value.
  If it's not an object then it outputs the rule and value
  as the CSS style rule and value.
*/
const template = _.template(`<% _.each(props, function(prop, classname) { %>.<%= classname %> {
<% _.each(prop, (value, rule) => { %><% if (typeof value === 'object') { %>  &.<%= rule %> {<% _.each(value, (subvalue, subrule) => { %>
    <%= subrule %>: <%= subvalue %>;<% }) %>
  }<% } else { %>  <%= rule %>: <%= value %>;<% } %>
<% }) %><% }) %>}
`)

const StyleDictionary = require('style-dictionary')
  .registerFormat({
    name: 'scss/button',
    formatter: function(dictionary, config) {
      /*
        allProperties is an array containing all the matched
        tokens based on the filter.
      */
      const { allProperties } = dictionary

      /*
        Set up an empty object to hold the final shape to pass
        to the custom template.

        After the allProperties.map(), props will look like this:
        {
          'component-button': {
            padding: '16px',
            'font-size': '16px',
            'text-align': 'center',
            primary: { 'background-color': '#e63c19', color: '#ffffff' },
            secondary: { 'background-color': '#fad8d1', color: '#0000ff' }
          }
        }
      */
      const props = {}

      // go through properties and structure final props object
      allProperties.map(prop => {
        /*
          Extract the attributes object created by the 'attribute/cti'
          transform and the transformed token value.
        */
        const { attributes, value } = prop

        // extract attributes to build custom class and style rules
        const { category, type, item, subitem } = attributes

        // build main classname for .scss file
        const classname = `${category}-${type}`

        /*
          Add to the props object if it doesn't already exist.
          We run the check to see if the classname exists already as an
          object property because in our case, `classname` will be the
          same for each token object in allProperties because each token
          is under the same category and type.
        */
        if (!props.hasOwnProperty(classname)) {
          props[classname] = {}
        }

        /*
          If the token object has a subitem, use the item as the subclass.
          Run the same check to see if this particular subclass (item) has
          been added yet.
        */
        if (subitem) {
          if (!props[classname].hasOwnProperty(item)) {
            props[classname][item] = {}
          }

          // add the subitem and value as final CSS rule
          props[classname][item][subitem] = value
        }
        else {
          // add the item as a CSS rule, not a subclass
          props[classname][item] = value
        }
      })

      /*
        Pass the final `props` object to our custom template to render
        the contents for the final button.scss file.
      */
      return template({ props })
    }
  })
  .extend({
    source: ['button.json'],
    platforms: {
      scss: {
        buildPath: 'build/',
        transforms: [
          'attribute/cti',  // setup attributes object
          'color/css',      // transform color values to hex
          'name/cti/kebab', // prevent name collisions
          'size/px'         // transform size values to px
        ],
        files: [
          {
            destination: 'button.scss',
            format: 'scss/button',
            filter: {
              attributes: {
                category: 'component',
                type: 'button'
              }
            }
          }
        ]
      }
    }
  })

// run Style Dictionary
StyleDictionary.buildAllPlatforms()

I made a quick sample project if you wanted to test it out:
https://github.com/davidyeiser/style-dictionary-scss-component

After you run yarn build you should see a button.scss file in the _build_ directory that looks like this:

.component-button {
  padding: 16px;
  font-size: 16px;
  text-align: center;
  &.primary {
    background-color: #e63c19;
    color: #ffffff;
  }
  &.secondary {
    background-color: #fad8d1;
    color: #0000ff;
  }
}

Let me know if you have any questions 鈥斅爃appy to answer them!

All 7 comments

(I posted an answer to your Stack Overflow question but thought I鈥檇 cross-post it here too.)

The way I would do this is create a custom format and template. I would also move all of the tokens that you want to be transformed out of the component category and use pointers instead. I did this for the sample button.json token file used to generate the final file.

The files needed to run the example are below.

button.json

{
  "color": {
    "background": {
      "primary": { "value": "hsl(10, 80, 50)" },
      "secondary": { "value": "hsl(10, 80, 90)" }
    },
    "font": {
      "inverse": { "value": "#fff" },
      "link": { "value": "#00f" }
    }
  },
  "size": {
    "2": { "value": 16 },
    "padding": {
      "medium": { "value": 16 }
    }
  },
  "component": {
    "button": {
      "padding": { "value": "{size.padding.medium.value}" },
      "font-size": { "value": "{size.2.value}" },
      "text-align": { "value": "center" },
      "primary": {
        "background-color": { "value": "{color.background.primary.value}" },
        "color": { "value": "{color.font.inverse.value}" }
      },
      "secondary": {
        "background-color": { "value": "{color.background.secondary.value}" },
        "color": { "value": "{color.font.link.value}" }
      }
    }
  }
}

index.js

const _ = require('lodash')

/*
  This is formatted oddly in order to get a nice final shape
  in `button.scss`.

  Essentially what the template is doing is looping through
  the props object and outputting the top-level properties
  as the parent classnames, then for each child props of
  "classname" it looks to see if the child prop is an object,
  if it is, then it outputs the Sass `&.` operator with the
  child prop rule as the sub classname and then each child
  prop of the value as the final CSS style rule and value.
  If it's not an object then it outputs the rule and value
  as the CSS style rule and value.
*/
const template = _.template(`<% _.each(props, function(prop, classname) { %>.<%= classname %> {
<% _.each(prop, (value, rule) => { %><% if (typeof value === 'object') { %>  &.<%= rule %> {<% _.each(value, (subvalue, subrule) => { %>
    <%= subrule %>: <%= subvalue %>;<% }) %>
  }<% } else { %>  <%= rule %>: <%= value %>;<% } %>
<% }) %><% }) %>}
`)

const StyleDictionary = require('style-dictionary')
  .registerFormat({
    name: 'scss/button',
    formatter: function(dictionary, config) {
      /*
        allProperties is an array containing all the matched
        tokens based on the filter.
      */
      const { allProperties } = dictionary

      /*
        Set up an empty object to hold the final shape to pass
        to the custom template.

        After the allProperties.map(), props will look like this:
        {
          'component-button': {
            padding: '16px',
            'font-size': '16px',
            'text-align': 'center',
            primary: { 'background-color': '#e63c19', color: '#ffffff' },
            secondary: { 'background-color': '#fad8d1', color: '#0000ff' }
          }
        }
      */
      const props = {}

      // go through properties and structure final props object
      allProperties.map(prop => {
        /*
          Extract the attributes object created by the 'attribute/cti'
          transform and the transformed token value.
        */
        const { attributes, value } = prop

        // extract attributes to build custom class and style rules
        const { category, type, item, subitem } = attributes

        // build main classname for .scss file
        const classname = `${category}-${type}`

        /*
          Add to the props object if it doesn't already exist.
          We run the check to see if the classname exists already as an
          object property because in our case, `classname` will be the
          same for each token object in allProperties because each token
          is under the same category and type.
        */
        if (!props.hasOwnProperty(classname)) {
          props[classname] = {}
        }

        /*
          If the token object has a subitem, use the item as the subclass.
          Run the same check to see if this particular subclass (item) has
          been added yet.
        */
        if (subitem) {
          if (!props[classname].hasOwnProperty(item)) {
            props[classname][item] = {}
          }

          // add the subitem and value as final CSS rule
          props[classname][item][subitem] = value
        }
        else {
          // add the item as a CSS rule, not a subclass
          props[classname][item] = value
        }
      })

      /*
        Pass the final `props` object to our custom template to render
        the contents for the final button.scss file.
      */
      return template({ props })
    }
  })
  .extend({
    source: ['button.json'],
    platforms: {
      scss: {
        buildPath: 'build/',
        transforms: [
          'attribute/cti',  // setup attributes object
          'color/css',      // transform color values to hex
          'name/cti/kebab', // prevent name collisions
          'size/px'         // transform size values to px
        ],
        files: [
          {
            destination: 'button.scss',
            format: 'scss/button',
            filter: {
              attributes: {
                category: 'component',
                type: 'button'
              }
            }
          }
        ]
      }
    }
  })

// run Style Dictionary
StyleDictionary.buildAllPlatforms()

I made a quick sample project if you wanted to test it out:
https://github.com/davidyeiser/style-dictionary-scss-component

After you run yarn build you should see a button.scss file in the _build_ directory that looks like this:

.component-button {
  padding: 16px;
  font-size: 16px;
  text-align: center;
  &.primary {
    background-color: #e63c19;
    color: #ffffff;
  }
  &.secondary {
    background-color: #fad8d1;
    color: #0000ff;
  }
}

Let me know if you have any questions 鈥斅爃appy to answer them!

Thank you for posting this answer @davidyeiser! That is definitely a great solution. I have done something similar in some projects, but instead used a custom action to build the files. I made a quick example repo as well: https://github.com/dbanksdesign/style-dictionary-example-component-scss (though it is not as thoroughly commented as david's).

Nice! I like the modifier and child attributes.

@sreenu539 did these examples answer you question? Could we close this issue?

@dbanksdesign
It seems that style-dictionary is so powerful that it can fully replace sass.
In a design system, have u ever tried write all tokens and components in style dictionary, and then output .css files directly?
we can write tokens source files in js, to use the sass equivalent vars/functions/mixins/nested.
The tradeoff is to write a lot of formatters.

I never actually thought about that... I have not tried, or seen anyone try, to write all styling in style dictionary and output to CSS directly. But it is definitely possible. You are right, the trade-off would be writing a lot of custom code in the form of formatters potentially. But I could also see writing a small handful of generic formatters as long as you structure your style dictionary object in a specific way. Talking through this, it kind of sounds like CSS-in-JS, but reversed... JS-in-CSS? You write styles in JS and output plain CSS (as opposed to the way CSS-in-JS libraries work today)...

you just blew my mind

@dbanksdesign
It seems that style-dictionary is so powerful that it can fully replace sass.
In a design system, have u ever tried write all tokens and components in style dictionary, and then output .css files directly?
we can write tokens source files in js, to use the sass equivalent vars/functions/mixins/nested.
The tradeoff is to write a lot of formatters.

This... really is mind-blowing. Something to think about more fully supporting in 4.0?

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dbanksdesign picture dbanksdesign  路  5Comments

matt-tyas picture matt-tyas  路  3Comments

tlouisse picture tlouisse  路  4Comments

dbanksdesign picture dbanksdesign  路  3Comments

chemoish picture chemoish  路  3Comments