[{"data":1,"prerenderedAt":548},["ShallowReactive",2],{"writing-figma-vue-handoff":3},{"id":4,"title":5,"body":6,"category":538,"date":539,"description":540,"extension":310,"i18nKey":541,"meta":542,"navigation":111,"path":543,"readingTime":544,"seo":545,"stem":546,"__hash__":547},"writing\u002Fwriting\u002Ffigma-vue-handoff.md","The Figma → Vue handoff I actually use",{"type":7,"value":8,"toc":531},"minimark",[9,27,30,35,46,175,191,198,202,221,224,239,354,357,361,367,452,469,473,476,504,507,511,518,527],[10,11,12,13,17,18,22,23,26],"p",{},"I sit on both sides of the handoff: I design in Figma and I build in Vue. That vantage point taught me something uncomfortable — most handoff pain isn't a tooling problem, it's a ",[14,15,16],"em",{},"naming"," problem. The design says ",[19,20,21],"code",{},"Primary\u002FHover",", the code says ",[19,24,25],{},"btn-main-alt",", and every conversation about the UI needs a translator.",[10,28,29],{},"Nothing below requires a plugin, a design-ops team, or a \"design system project\". It's a handful of conventions I apply on every project, including solo ones — because the person translating my own Figma file into code three weeks later is me, with no memory of what I meant.",[31,32,34],"h2",{"id":33},"tokens-live-in-one-place-and-that-place-is-css","Tokens live in one place, and that place is CSS",[10,36,37,38,41,42,45],{},"Every value that Figma calls a ",[14,39,40],{},"variable"," — colors, type scale, radii, spacing — becomes a CSS variable in code. With Tailwind v4 this is delightfully direct, because the theme ",[14,43,44],{},"is"," CSS:",[47,48,53],"pre",{"className":49,"code":50,"language":51,"meta":52,"style":52},"language-css shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u002F* main.css *\u002F\n@theme static {\n  --font-sans: 'Inter', sans-serif;\n  --font-serif: 'Instrument Serif', serif;\n\n  --color-primary: #a11ee2;\n  --color-secondary: #fac789;\n  --color-heading: #0f172b;\n  --color-body: #45556c;\n\n  --radius-card: 1rem;\n}\n","css","",[19,54,55,64,79,95,106,113,125,136,147,158,163,169],{"__ignoreMap":52},[56,57,60],"span",{"class":58,"line":59},"line",1,[56,61,63],{"class":62},"sHwdD","\u002F* main.css *\u002F\n",[56,65,67,71,75],{"class":58,"line":66},2,[56,68,70],{"class":69},"s7zQu","@theme",[56,72,74],{"class":73},"sTEyZ"," static ",[56,76,78],{"class":77},"sMK4o","{\n",[56,80,82,85,88,92],{"class":58,"line":81},3,[56,83,84],{"class":73},"  --font-sans: 'Inter'",[56,86,87],{"class":77},",",[56,89,91],{"class":90},"sBMFI"," sans-serif",[56,93,94],{"class":73},";\n",[56,96,98,101,103],{"class":58,"line":97},4,[56,99,100],{"class":73},"  --font-serif: 'Instrument Serif'",[56,102,87],{"class":77},[56,104,105],{"class":73}," serif;\n",[56,107,109],{"class":58,"line":108},5,[56,110,112],{"emptyLinePlaceholder":111},true,"\n",[56,114,116,119,122],{"class":58,"line":115},6,[56,117,118],{"class":73},"  --color-primary: ",[56,120,121],{"class":77},"#",[56,123,124],{"class":73},"a11ee2;\n",[56,126,128,131,133],{"class":58,"line":127},7,[56,129,130],{"class":73},"  --color-secondary: ",[56,132,121],{"class":77},[56,134,135],{"class":73},"fac789;\n",[56,137,139,142,144],{"class":58,"line":138},8,[56,140,141],{"class":73},"  --color-heading: ",[56,143,121],{"class":77},[56,145,146],{"class":73},"0f172b;\n",[56,148,150,153,155],{"class":58,"line":149},9,[56,151,152],{"class":73},"  --color-body: ",[56,154,121],{"class":77},[56,156,157],{"class":73},"45556c;\n",[56,159,161],{"class":58,"line":160},10,[56,162,112],{"emptyLinePlaceholder":111},[56,164,166],{"class":58,"line":165},11,[56,167,168],{"class":73},"  --radius-card: 1rem;\n",[56,170,172],{"class":58,"line":171},12,[56,173,174],{"class":73},"}\n",[10,176,177,178,182,183,186,187,190],{},"The rule that makes this work: ",[179,180,181],"strong",{},"the Figma variable name and the CSS variable name are the same name."," ",[19,184,185],{},"color\u002Fprimary"," ↔ ",[19,188,189],{},"--color-primary",". When the designer (me, or a client's designer) renames or retunes a token, the diff in code is one line, and nobody has to reverse-engineer which of five near-identical purples this button was supposed to use.",[10,192,193,194,197],{},"The anti-pattern is hex values scattered through templates. The moment a ",[19,195,196],{},"#a11ee2"," appears inline, that element has silently left the design system, and no Figma update will ever reach it again.",[31,199,201],{"id":200},"components-map-11-same-name-same-boundaries","Components map 1:1 — same name, same boundaries",[10,203,204,205,208,209,212,213,216,217,220],{},"If the Figma file has a component called ",[19,206,207],{},"ProjectCard",", the repo has ",[19,210,211],{},"ProjectCard.vue",". Not ",[19,214,215],{},"WorkItem.vue",", not a ",[19,218,219],{},"Card.vue"," with fifteen conditional slots. Same name, same boundary.",[10,222,223],{},"This sounds cosmetic. It isn't. The component boundary is where every conversation about a UI happens — \"the testimonial card looks off on mobile\" — and when boundaries match, that sentence points at exactly one file and one Figma frame. When they don't, you get archaeology.",[10,225,226,227,230,231,234,235,238],{},"The corollary: ",[179,228,229],{},"variants become props."," A Figma component with variants ",[19,232,233],{},"size: sm | md"," and ",[19,236,237],{},"tone: default | featured"," becomes:",[47,240,244],{"className":241,"code":242,"language":243,"meta":52,"style":52},"language-vue shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","\u003Cscript setup lang=\"ts\">\ndefineProps\u003C{\n  size?: 'sm' | 'md'\n  tone?: 'default' | 'featured'\n}>()\n\u003C\u002Fscript>\n","vue",[19,245,246,277,286,314,337,345],{"__ignoreMap":52},[56,247,248,251,255,259,262,265,268,272,274],{"class":58,"line":59},[56,249,250],{"class":77},"\u003C",[56,252,254],{"class":253},"swJcz","script",[56,256,258],{"class":257},"spNyl"," setup",[56,260,261],{"class":257}," lang",[56,263,264],{"class":77},"=",[56,266,267],{"class":77},"\"",[56,269,271],{"class":270},"sfazB","ts",[56,273,267],{"class":77},[56,275,276],{"class":77},">\n",[56,278,279,283],{"class":58,"line":66},[56,280,282],{"class":281},"s2Zo4","defineProps",[56,284,285],{"class":77},"\u003C{\n",[56,287,288,291,294,297,300,303,306,308,311],{"class":58,"line":81},[56,289,290],{"class":253},"  size",[56,292,293],{"class":77},"?:",[56,295,296],{"class":77}," '",[56,298,299],{"class":270},"sm",[56,301,302],{"class":77},"'",[56,304,305],{"class":77}," |",[56,307,296],{"class":77},[56,309,310],{"class":270},"md",[56,312,313],{"class":77},"'\n",[56,315,316,319,321,323,326,328,330,332,335],{"class":58,"line":97},[56,317,318],{"class":253},"  tone",[56,320,293],{"class":77},[56,322,296],{"class":77},[56,324,325],{"class":270},"default",[56,327,302],{"class":77},[56,329,305],{"class":77},[56,331,296],{"class":77},[56,333,334],{"class":270},"featured",[56,336,313],{"class":77},[56,338,339,342],{"class":58,"line":108},[56,340,341],{"class":77},"}>",[56,343,344],{"class":73},"()\n",[56,346,347,350,352],{"class":58,"line":115},[56,348,349],{"class":77},"\u003C\u002F",[56,351,254],{"class":253},[56,353,276],{"class":77},[10,355,356],{},"Same axis names, same option names. If a variant combination doesn't exist in Figma, it shouldn't be expressible in code — that's what the union types are for. I've come to treat a prop that has no corresponding variant as a smell: either the design is missing a state, or the code grew an option nobody designed.",[31,358,360],{"id":359},"auto-layout-is-a-flexbox-spec-read-it-as-one","Auto layout is a flexbox spec — read it as one",[10,362,363,364,366],{},"Figma's auto layout panel ",[14,365,44],{}," the flex spec, written in different words. The mapping is mechanical:",[368,369,370,383],"table",{},[371,372,373],"thead",{},[374,375,376,380],"tr",{},[377,378,379],"th",{},"Figma",[377,381,382],{},"CSS",[384,385,386,398,408,418,426,440],"tbody",{},[374,387,388,392],{},[389,390,391],"td",{},"Direction: horizontal",[389,393,394,397],{},[19,395,396],{},"flex"," (row)",[374,399,400,403],{},[389,401,402],{},"Gap: 24",[389,404,405],{},[19,406,407],{},"gap-6",[374,409,410,413],{},[389,411,412],{},"Padding: 16 \u002F 24",[389,414,415],{},[19,416,417],{},"px-6 py-4",[374,419,420,423],{},[389,421,422],{},"Hug contents",[389,424,425],{},"(natural size — no class)",[374,427,428,431],{},[389,429,430],{},"Fill container",[389,432,433,436,437],{},[19,434,435],{},"flex-1"," \u002F ",[19,438,439],{},"w-full",[374,441,442,445],{},[389,443,444],{},"Fixed width",[389,446,447,448,451],{},"a real ",[19,449,450],{},"w-*"," — and a question",[10,453,454,455,458,459,461,462,465,466,468],{},"Two habits on top of the table. First, ",[179,456,457],{},"snap Figma values to the spacing scale"," — a gap of 24 is ",[19,460,407],{},", a gap of 23 is a mistake in the file, not a ",[19,463,464],{},"gap-[23px]"," in the code. Arbitrary values are how a design system dies one pixel at a time. Second, treat ",[14,467,444],{}," as a prompt to ask \"what happens when the content grows?\" — fixed sizes in Figma are usually placeholders for a responsive rule the design hasn't stated yet. Better to resolve that at handoff than in a bug report.",[31,470,472],{"id":471},"spec-states-not-screenshots","Spec states, not screenshots",[10,474,475],{},"The static frame is the least interesting part of a handoff. What the build actually needs, and what I write as a short block of notes next to each component in the Figma file:",[477,478,479,486,492,498],"ul",{},[480,481,482,485],"li",{},[179,483,484],{},"Hover \u002F focus \u002F active"," — even just \"darken 5%, ring on focus\"",[480,487,488,491],{},[179,489,490],{},"Empty, loading, error"," — the states everyone designs last and users see first",[480,493,494,497],{},[179,495,496],{},"Text overflow"," — does the title truncate, wrap, or push the layout?",[480,499,500,503],{},[179,501,502],{},"Motion intent"," — one line, like \"card lifts on hover, fades in on scroll, 300ms\"",[10,505,506],{},"Five minutes of writing per component. It replaces the entire class of \"the design didn't say\" bugs, which in my experience outnumber the \"the code got it wrong\" bugs comfortably.",[31,508,510],{"id":509},"the-point-is-fewer-translations","The point is fewer translations",[10,512,513,514,517],{},"None of this is a process. There's no sync meeting, no token pipeline, no plugin subscription. It's one principle applied everywhere: ",[179,515,516],{},"design and code should use the same words for the same things"," — token names, component names, variant names, spacing steps. Every place the vocabularies match is a translation that no longer needs to happen, and every translation you delete is a bug that can't be introduced.",[10,519,520,521,526],{},"I offer this end to end — same person designing the file and shipping the Nuxt app, so the handoff cost rounds to zero. If that's the kind of help your project needs, ",[522,523,525],"a",{"href":524},"\u002F#contact","let's talk",".",[528,529,530],"style",{},"html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sBMFI, html code.shiki .sBMFI{--shiki-light:#E2931D;--shiki-default:#FFCB6B;--shiki-dark:#FFCB6B}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}",{"title":52,"searchDepth":66,"depth":66,"links":532},[533,534,535,536,537],{"id":33,"depth":66,"text":34},{"id":200,"depth":66,"text":201},{"id":359,"depth":66,"text":360},{"id":471,"depth":66,"text":472},{"id":509,"depth":66,"text":510},"Frontend","2026-08-03","Tokens as CSS variables, components named 1:1, variants that map to props — the small conventions that keep design and code in sync without a process tax.","figmaVue",{},"\u002Fwriting\u002Ffigma-vue-handoff","7 min read",{"title":5,"description":540},"writing\u002Ffigma-vue-handoff","dMPgLfGRvvj_dO8MnGCCzWQCL5-N55LaiO69Hog7c6Y",1785786127680]