Skip to content

Instantly share code, notes, and snippets.

@brookback
Created March 30, 2019 12:29
Show Gist options
  • Star 38 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save brookback/cce828202ef40ddddf2d1aa731929c90 to your computer and use it in GitHub Desktop.
Save brookback/cce828202ef40ddddf2d1aa731929c90 to your computer and use it in GitHub Desktop.
/*
* This script fetches all color styles from a Figma team/document.
*
* Dependencies:
*
* - node-fetch
*
* Due to a limitation in the Figma /styles endpoint, we need to use a
* document for actually using the colors in a color grid 🙄That's why
* we're both fetching from /styles and /files below.
*
* For now, you need to input the page and team IDs, as well as the file keys.
* The team ID is in the Figma URL of your team, and the file key is the long
* string in the full URL of a Figma file. The page ID is visible in the JSON
* payload when you call /files 🤷‍♂️
*/
// Inspect the /files JSON response, or the URL of the Figma page:
// https://www.figma.com/file/<file key>/Some-Name?node-id=<encoded page ID, like '183%3A0 = 183:0'>
const PAGE_ID = '<page id, like 183:0>';
// Go to a team URL and get the ID:
// https://www.figma.com/files/team/<team id>/Team-Name
const TEAM_ID = '<team id here>';
// Get this from the URL of a single file:
// https://www.figma.com/file/<file key>/Some-Name?node-id=182%3A0
const FILE_KEY = '<file key here>';
const fetch = require('node-fetch');
const fs = require('fs');
const { promisify } = require('util');
const path = require('path');
const writeFile = promisify(fs.writeFile);
const personalToken = process.env.FIGMA_PERSONAL_TOKEN;
if (!personalToken) {
console.error('Please pass FIGMA_PERSONAL_TOKEN to this script and re-run');
process.exit(1);
}
const figmaBase = 'https://api.figma.com/';
const rgbToHex = (r, g, b) =>
'#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
const slugify = (str) => str.toLowerCase().replace(/\s+/, '-');
const doFetch = (url) =>
fetch(`${figmaBase}v1${url}`, {
headers: {
'X-Figma-Token': personalToken,
},
})
.then((res) => {
if (!res.ok) {
throw new Error(`Status: ${res.status}`);
}
return res.json();
})
.then((json) => {
if (json.error || (json.status && json.status !== 200)) {
throw new Error(
json.error || `Status ${json.status}: ${json.err}`
);
}
return json;
});
const fetchStyles = async (teamId) => {
const json = await doFetch(`/teams/${teamId}/styles?page_size=99`);
return json.meta.styles;
};
const fetchFile = async (key) => await doFetch(`/files/${key}`);
// eslint-disable-next-line
const fetchStyle = async (key) => await doFetch(`/styles/${key}`);
/**
* Fetches all color styles from the Figma doc and returns an object
* array with this shape:
*
* ```json
* {
* "key": "206f4e4753e2e8f2e7ac24744bd1843ac206ead1",
* "file_key": "bMb57SxaX0ugGmWMmi7KVzIP",
* "node_id": "189:85",
* "style_type": "FILL",
* "thumbnail_url": "<url>",
* "name": "Green 10",
* "description": "Desc",
* "created_at": "2019-02-16T16:00:39.126Z",
* "updated_at": "2019-02-16T16:00:39.126Z",
* "user": {
* "id": "575212366706412863",
* "handle": "Johan Brook",
* "img_url": "<url>"
* },
* "sort_position": "=O",
* "color": "#ebfff7"
* }
```
*/
const fetchAllColorStyles = async () => {
const styles = await fetchStyles(TEAM_ID);
const file = await fetchFile(FILE_KEY);
const canvas = file.document.children.find((page) => page.id === PAGE_ID);
return (
canvas &&
canvas.children
.filter((c) => c.type === 'INSTANCE')
.map((c) => c.children.filter((c) => c.type === 'RECTANGLE')[0])
.filter((c) => !!c.styles && !!c.styles.fill)
.map((c) => {
const { r, g, b } = c.fills[0].color;
const nodeId = c.styles.fill;
return {
// Cross reference to the array of styles, since Figma doesn't
// give us the HEX color codes in their /styles endpoint .. :(
...styles.find((s) => s.node_id === nodeId),
color: rgbToHex(r * 255, g * 255, b * 255),
};
})
.filter((c) => !!c.name)
);
};
/**
* Calls Figma's API and saves to a `colors.js` file in the project root.
*/
const writeColorsFromFigma = async () => {
const styles = await fetchAllColorStyles();
if (!styles) {
throw new Error('No styles found');
}
const colors = styles
.sort((a, b) => (a.sort_position < b.sort_position ? -1 : 1))
.map(
(s) =>
(s.description ? ` /** ${s.description} */\n` : '') +
` '${slugify(s.name)}': '${s.color}',`
)
.join('\n');
const fileContents = `/* eslint-disable */
/* Updated at ${new Date().toUTCString()}*/
module.exports = {
${colors}
}`;
await writeFile(path.resolve(__dirname + './colors.js'), fileContents);
console.log(`Wrote ${styles.length} colors to colors.js`);
};
writeColorsFromFigma().catch(console.error);
@risros
Copy link

risros commented Feb 7, 2020

Good job!
But why are you doing calls to /teams/ when you have all information inside /files/:fileid response.
You get style "id", e.g. "2:2"
And then you can search for
"styles":{
"fill":"2:2"
}
and see the RGB of the parent object (you take the first one)

@TupotaValentyn
Copy link

TupotaValentyn commented Feb 13, 2021

it is not exactly for current figma-api, if you need to convert rgb to hex, you need to multiply on 256 and after to take first seven symbols from result :

function rgbToHex(r, g, b) {
  const color = '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);

  if (color.length > 7) {
    return color.slice(0, 7);
  }
  return color;
};

 const hex = rgbToHex(r * 256, g * 256, b * 256);

@Utzel-Butzel
Copy link

Thanks a lot for your blog post and script! It now also works without getting the Team API endpoint. This can be useful if the personal token only has limited access.
Also added a recursive search for rectangles and ellipsis to make allow deep-nested color fields.

/*
 * This script fetches all color styles from a Figma document.
 *
 * Dependencies:
 *
 *  - node-fetch
 *
 *
 * For now, you need to input the page ID, as well as the file keys.
 * The page ID is visible in the JSON
 * payload when you call /files 🤷‍♂️
 */
// Inspect the /files JSON response, or the URL of the Figma page:
// https://www.figma.com/file/<file key>/Some-Name?node-id=<encoded page ID, like '183%3A0 = 183:0'>

require('dotenv').config();
const PAGE_ID = '0:1';
// Get this from the URL of a single file:
// https://www.figma.com/file/<file key>/Some-Name?node-id=182%3A0
const FILE_KEY = process.env.FILE_LEY;

const fetch = require('node-fetch');
const fs = require('fs');
const { promisify } = require('util');
const path = require('path');

const writeFile = promisify(fs.writeFile);

const personalToken = process.env.DEV_ACCESS_TOKEN;

if (!personalToken) {
  console.error('Please pass FIGMA_PERSONAL_TOKEN to this script and re-run');
  process.exit(1);
}

const figmaBase = 'https://api.figma.com/';

function rgbToHex(r, g, b) {
  const color =
    '#' + ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);

  if (color.length > 7) {
    return color.slice(0, 7);
  }
  return color;
}

const slugify = (str) => str.toLowerCase().replace(/\s+/, '-');

const doFetch = (url) =>
  fetch(`${figmaBase}v1${url}`, {
    headers: {
      'X-Figma-Token': personalToken,
    },
  })
    .then((res) => {
      if (!res.ok) {
        console.log(`${figmaBase}v1${url}`);
        throw new Error(`Status: ${res.status}`);
      }

      return res.json();
    })
    .then((json) => {
      if (json.error || (json.status && json.status !== 200)) {
        throw new Error(json.error || `Status ${json.status}: ${json.err}`);
      }

      return json;
    });

const fetchFile = async (key) => await doFetch(`/files/${key}`);

const fetchAllColorStyles = async () => {
  const file = await fetchFile(FILE_KEY);

  const styles = Object.entries(file.styles);
  console.log(Object.entries(styles));

  const canvas = file.document.children.find((page) => page.id === PAGE_ID);

  var colorList = [];

  const runCanvas = (ca) => {
    ca.forEach((c) => {
      if (c.type === 'RECTANGLE' || c.type === 'ELLIPSE') {
        const { r, g, b } = c.fills[0].color;
        const nodeId = c.styles.fill;

        const foundStyles = styles.find(([node_id]) => node_id === nodeId);
        if (foundStyles)
          colorList.push({
            // Cross reference to the array of styles, since Figma doesn't
            // give us the HEX color codes in their /styles endpoint .. :(
            ...foundStyles[1],
            color: rgbToHex(r * 256, g * 256, b * 256),
          });
      }
      if (c.children !== undefined && Array.isArray(c.children)) {
        runCanvas(c.children);
      }
    });
  };
  runCanvas(canvas.children);

  return colorList;
  return (
    canvas &&
    canvas.children
      .filter((c) => c.type === 'INSTANCE')
      .map((c) => c.children.filter((c) => c.type === 'RECTANGLE')[0])
      .filter((c) => !!c.styles && !!c.styles.fill)
      .map((c) => {
        console.log(c.fills[0].color);
        const { r, g, b } = c.fills[0].color;
        const nodeId = c.styles.fill;

        return {
          // Cross reference to the array of styles, since Figma doesn't
          // give us the HEX color codes in their /styles endpoint .. :(
          ...styles.find((s) => s.node_id === nodeId),
          color: rgbToHex(r * 256, g * 256, b * 256),
        };
      })
      .filter((c) => !!c.name)
  );
};

/**
 * Calls Figma's API and saves to a `colors.js` file in the project root.
 */
const writeColorsFromFigma = async () => {
  const styles = await fetchAllColorStyles();
  console.log(styles);

  if (!styles) {
    throw new Error('No styles found');
  }

  const colors = styles
    .sort((a, b) => (a.sort_position < b.sort_position ? -1 : 1))
    .map(
      (s) =>
        (s.description ? `    /** ${s.description} */\n` : '') +
        `    '${slugify(s.name)}': '${s.color}',`
    )
    .join('\n');

  const fileContents = `/* eslint-disable */
/* Updated at ${new Date().toUTCString()}*/
module.exports = {
${colors}
}`;

  await writeFile(path.resolve(__dirname + '/colors-export.js'), fileContents);

  console.log(`Wrote ${styles.length} colors to colors.js`);
};

writeColorsFromFigma().catch(console.error);

@danielgolden
Copy link

Thanks so much for publishing this! It's a huge gift. Out of curiosity, why not loop through all pages in the file?

@ivancantarino
Copy link

Thanks for sharing this. I am wondering what would be the best way to export as RGB (or RGBA), instead of HEX values.
Would it be possible? I only see the option to export as HEX values in the code above 😇

@brookback
Copy link
Author

@ivancantarino Adjust this line to not call rgbToHex:

color: rgbToHex(r * 256, g * 256, b * 256),

@ivancantarino
Copy link

oh awesome! Thanks for the fast reply @brookback!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment