Skip to content

Instantly share code, notes, and snippets.

@nautilytics
Created October 10, 2025 15:48
Show Gist options
  • Select an option

  • Save nautilytics/43a63ecf7ea89f7de6d1f3ce272caffb to your computer and use it in GitHub Desktop.

Select an option

Save nautilytics/43a63ecf7ea89f7de6d1f3ce272caffb to your computer and use it in GitHub Desktop.
Bring Percy snapshots into Figma
// This file holds the main code for plugins. Code in this file has access to
// the *figma document* via the figma global object.
// You can access browser APIs in the <script> tag inside "ui.html" which has a
// full browser environment (See https://www.figma.com/plugin-docs/how-plugins-run).
const PERCY_TOKEN = ''
const BASE_PERCY_API_URL = ''
type PercyBuild = {
data: [{ id: string }]
}
type PercySnapshots = {
data: [{ attributes: { name: string }; type: 'snapshots'; id: string }]
}
type PercySnapshot = {
included: [
{
type: 'images' | 'browser-families'
attributes: { slug: string; shortName: string; url: string }
relationships: { image: { data: { id: string } } }
id: string
}
]
}
const getData = async (url: string) => {
try {
const response = await fetch(url, {
method: 'GET',
headers: {
Authorization: `Token token=${PERCY_TOKEN}`,
},
})
const data = await response.json()
return data
} catch (error) {
console.error('Failed to retrieve data')
return
}
}
figma.showUI(__html__)
// Calls to "parent.postMessage" from within the HTML page will trigger this
// callback. The callback will be passed the "pluginMessage" property of the
// posted message.
figma.ui.onmessage = async (msg) => {
if (msg.type === 'update-percy-snapshots') {
const nodes: SceneNode[] = []
// Search for all the rectangles in the current page.
const allRectangles = figma.currentPage.findAllWithCriteria({
types: ['RECTANGLE'],
})
const nodeNames = allRectangles.map((node) => node.name)
const releaseBuilds = (await getData(`${BASE_PERCY_API_URL}/builds`)) as PercyBuild
if (releaseBuilds.data.length) {
const releaseBuildId = releaseBuilds.data[0].id
// Get snapshots for release build
const { data } = (await getData(
`${BASE_PERCY_API_URL}/snapshots?build_id=${releaseBuildId}`
)) as PercySnapshots
const availableSnapshots = nodeNames.map((node) => node.replace(/\s-\smobile/g, ''))
// Go through each available snapshot name and get the image links
for (const availableSnapshot of availableSnapshots) {
const selectedSnapshot = data.find(
(row) => row.type === 'snapshots' && row.attributes.name === availableSnapshot
)
// Get snapshot details
const { included } = (await getData(
`${BASE_PERCY_API_URL}/snapshots/${selectedSnapshot!.id}`
)) as PercySnapshot
// Get image links for mobile snapshots
const browser = { slug: 'chrome_on_android', shortName: 'Mobile' }
const snapshotId = `${availableSnapshot} - ${browser.shortName.toLowerCase()}`
if (included.length > 0) {
const browserFamilyIdx = included.findIndex(
(row) => row.type === 'browser-families' && row.attributes.slug === browser.slug
)
const screenshotImageId = included[browserFamilyIdx + 1].relationships.image.data.id // the screenshot for the browser family is always the next row
const screenshotImageUrl = included.find(
(row) => row.type === 'images' && row.id === screenshotImageId
)?.attributes?.url
if (screenshotImageUrl) {
let image
try {
image = await figma.createImageAsync(screenshotImageUrl)
} catch (error) {
console.log(`Error with ${snapshotId} skipping`)
console.error(error)
continue
}
// Go through each image on the page and replace the image with the most recent image
for (let i = 0; i < allRectangles.length; i++) {
const node = allRectangles[i]
if (node.name === snapshotId) {
console.log(`Updating ${snapshotId} rectangle`)
const { width, height } = await image.getSizeAsync()
node.resize(width, height)
node.fills = [
{
type: 'IMAGE',
imageHash: image.hash,
scaleMode: 'FILL',
},
]
}
nodes.push(node)
}
}
}
}
// Zoom to the current selection of nodes
figma.currentPage.selection = nodes
figma.viewport.scrollAndZoomIntoView(nodes)
// Make sure to close the plugin when you're done. Otherwise the plugin will
// keep running, which shows the cancel button at the bottom of the screen.
figma.closePlugin()
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment