[{"data":1,"prerenderedAt":926},["ShallowReactive",2],{"post-\u002Fposts\u002Fcss-variables-as-api":3,"post-nav-\u002Fposts\u002Fcss-variables-as-api":896,"post-related-\u002Fposts\u002Fcss-variables-as-api":906},{"id":4,"title":5,"body":6,"category":881,"cover":882,"date":883,"description":884,"draft":885,"extension":886,"featured":885,"meta":887,"navigation":103,"path":888,"pinned":885,"seo":889,"stem":890,"tags":891,"updated":882,"__hash__":895},"posts\u002Fposts\u002Fcss-variables-as-api.md","我把 CSS 变量当成了 API",{"type":7,"value":8,"toc":871},"minimark",[9,18,30,35,45,51,274,285,288,295,328,339,357,363,434,444,447,451,454,505,511,515,518,521,527,533,539,542,726,733,761,764,767,773,810,813,816,819,822,828,837,853,856,865,868],[10,11,12,13,17],"p",{},"设计系统最难的部分不是\"定义一套变量\"，是",[14,15,16],"strong",{},"保证没人绕过它","。",[10,19,20,21,25,26,29],{},"你可以写一份完美的 ",[22,23,24],"code",{},"tokens.css","，但只要有人在某个组件里写了 ",[22,27,28],{},"style=\"margin-top: 24px\"","，系统就开始腐烂了。",[31,32,34],"h2",{"id":33},"我的做法让-token-只有一个来源","我的做法：让 token 只有一个来源",[36,37,42],"pre",{"className":38,"code":40,"language":41},[39],"language-text","app\u002Ftheme.config.ts   ← 唯一的真相源（TypeScript 对象）\n        ↓ 构建期编译\ntokens.generated.css  ← 自动生成，不手改\n        ↓\n组件里的 var(--accent)  ← 只读变量\n","text",[22,43,40],{"__ignoreMap":44},"",[10,46,47,48],{},"关键在中间那一步：",[14,49,50],{},"CSS 不是手写的，是生成的。",[36,52,56],{"className":53,"code":54,"language":55,"meta":44,"style":44},"language-ts shiki shiki-themes github-light github-dark","\u002F\u002F modules\u002Fdesign-tokens.ts（简化）\nconst theme = await import('..\u002Fapp\u002Ftheme.config.ts')\n\nconst css = `\n:root {\n  --accent: ${theme.accent.light};\n  --background: ${theme.neutral.light.background};\n  --content-max-width: ${theme.contentMaxWidth};\n  --duration-base: ${theme.animation.duration}ms;\n}\n\n.dark {\n  --accent: ${theme.accent.dark};\n  --background: ${theme.neutral.dark.background};\n}\n`\n","ts",[22,57,58,67,98,105,118,124,147,171,186,207,213,218,224,242,263,268],{"__ignoreMap":44},[59,60,63],"span",{"class":61,"line":62},"line",1,[59,64,66],{"class":65},"sJ8bj","\u002F\u002F modules\u002Fdesign-tokens.ts（简化）\n",[59,68,70,74,78,81,84,87,91,95],{"class":61,"line":69},2,[59,71,73],{"class":72},"szBVR","const",[59,75,77],{"class":76},"sj4cs"," theme",[59,79,80],{"class":72}," =",[59,82,83],{"class":72}," await",[59,85,86],{"class":72}," import",[59,88,90],{"class":89},"sVt8B","(",[59,92,94],{"class":93},"sZZnC","'..\u002Fapp\u002Ftheme.config.ts'",[59,96,97],{"class":89},")\n",[59,99,101],{"class":61,"line":100},3,[59,102,104],{"emptyLinePlaceholder":103},true,"\n",[59,106,108,110,113,115],{"class":61,"line":107},4,[59,109,73],{"class":72},[59,111,112],{"class":76}," css",[59,114,80],{"class":72},[59,116,117],{"class":93}," `\n",[59,119,121],{"class":61,"line":120},5,[59,122,123],{"class":93},":root {\n",[59,125,127,130,133,136,139,141,144],{"class":61,"line":126},6,[59,128,129],{"class":93},"  --accent: ${",[59,131,132],{"class":89},"theme",[59,134,135],{"class":93},".",[59,137,138],{"class":89},"accent",[59,140,135],{"class":93},[59,142,143],{"class":89},"light",[59,145,146],{"class":93},"};\n",[59,148,150,153,155,157,160,162,164,166,169],{"class":61,"line":149},7,[59,151,152],{"class":93},"  --background: ${",[59,154,132],{"class":89},[59,156,135],{"class":93},[59,158,159],{"class":89},"neutral",[59,161,135],{"class":93},[59,163,143],{"class":89},[59,165,135],{"class":93},[59,167,168],{"class":89},"background",[59,170,146],{"class":93},[59,172,174,177,179,181,184],{"class":61,"line":173},8,[59,175,176],{"class":93},"  --content-max-width: ${",[59,178,132],{"class":89},[59,180,135],{"class":93},[59,182,183],{"class":89},"contentMaxWidth",[59,185,146],{"class":93},[59,187,189,192,194,196,199,201,204],{"class":61,"line":188},9,[59,190,191],{"class":93},"  --duration-base: ${",[59,193,132],{"class":89},[59,195,135],{"class":93},[59,197,198],{"class":89},"animation",[59,200,135],{"class":93},[59,202,203],{"class":89},"duration",[59,205,206],{"class":93},"}ms;\n",[59,208,210],{"class":61,"line":209},10,[59,211,212],{"class":93},"}\n",[59,214,216],{"class":61,"line":215},11,[59,217,104],{"emptyLinePlaceholder":103},[59,219,221],{"class":61,"line":220},12,[59,222,223],{"class":93},".dark {\n",[59,225,227,229,231,233,235,237,240],{"class":61,"line":226},13,[59,228,129],{"class":93},[59,230,132],{"class":89},[59,232,135],{"class":93},[59,234,138],{"class":89},[59,236,135],{"class":93},[59,238,239],{"class":89},"dark",[59,241,146],{"class":93},[59,243,245,247,249,251,253,255,257,259,261],{"class":61,"line":244},14,[59,246,152],{"class":93},[59,248,132],{"class":89},[59,250,135],{"class":93},[59,252,159],{"class":89},[59,254,135],{"class":93},[59,256,239],{"class":89},[59,258,135],{"class":93},[59,260,168],{"class":89},[59,262,146],{"class":93},[59,264,266],{"class":61,"line":265},15,[59,267,212],{"class":93},[59,269,271],{"class":61,"line":270},16,[59,272,273],{"class":93},"`\n",[10,275,276,277,280,281,284],{},"这样如果有人想改强调色，他",[14,278,279],{},"只能","去改 ",[22,282,283],{},"theme.config.ts","——因为 CSS 文件里改的东西下次构建就没了。",[31,286,287],{"id":287},"一个必须解决的坑",[10,289,290,291,294],{},"Tailwind v4 用 ",[22,292,293],{},"@theme"," 块定义工具类：",[36,296,300],{"className":297,"code":298,"language":299,"meta":44,"style":44},"language-css shiki shiki-themes github-light github-dark","@theme {\n  --color-accent: #a94f33;\n}\n","css",[22,301,302,309,324],{"__ignoreMap":44},[59,303,304,306],{"class":61,"line":62},[59,305,293],{"class":72},[59,307,308],{"class":89}," {\n",[59,310,311,315,318,321],{"class":61,"line":69},[59,312,314],{"class":313},"s4XuR","  --color-accent",[59,316,317],{"class":89},": ",[59,319,320],{"class":76},"#a94f33",[59,322,323],{"class":89},";\n",[59,325,326],{"class":61,"line":100},[59,327,212],{"class":89},[10,329,330,331,334,335,338],{},"这会生成 ",[22,332,333],{},"bg-accent","、",[22,336,337],{},"text-accent"," 等工具类。",[10,340,341,344,345,348,349,352,353,356],{},[14,342,343],{},"但如果你把 hex 直接写进去，暗色模式就失效了","——因为 ",[22,346,347],{},"--color-accent"," 被编译进了工具类，",[22,350,351],{},".dark"," 里覆盖 ",[22,354,355],{},"--accent"," 不会影响它。",[10,358,359,360,362],{},"正确做法是让 ",[22,361,293],{}," 指向动态变量：",[36,364,366],{"className":297,"code":365,"language":299,"meta":44,"style":44},"@theme {\n  --color-accent: var(--accent);  \u002F* 不是 hex *\u002F\n}\n\n:root { --accent: #a94f33; }\n.dark { --accent: #cc785c; }\n",[22,367,368,374,393,397,401,419],{"__ignoreMap":44},[59,369,370,372],{"class":61,"line":62},[59,371,293],{"class":72},[59,373,308],{"class":89},[59,375,376,378,380,383,385,387,390],{"class":61,"line":69},[59,377,314],{"class":313},[59,379,317],{"class":89},[59,381,382],{"class":76},"var",[59,384,90],{"class":89},[59,386,355],{"class":313},[59,388,389],{"class":89},");  ",[59,391,392],{"class":65},"\u002F* 不是 hex *\u002F\n",[59,394,395],{"class":61,"line":100},[59,396,212],{"class":89},[59,398,399],{"class":61,"line":107},[59,400,104],{"emptyLinePlaceholder":103},[59,402,403,407,410,412,414,416],{"class":61,"line":120},[59,404,406],{"class":405},"sScJk",":root",[59,408,409],{"class":89}," { ",[59,411,355],{"class":313},[59,413,317],{"class":89},[59,415,320],{"class":76},[59,417,418],{"class":89},"; }\n",[59,420,421,423,425,427,429,432],{"class":61,"line":126},[59,422,351],{"class":405},[59,424,409],{"class":89},[59,426,355],{"class":313},[59,428,317],{"class":89},[59,430,431],{"class":76},"#cc785c",[59,433,418],{"class":89},[10,435,436,437,439,440,443],{},"这样 ",[22,438,333],{}," 编译出的 CSS 是 ",[22,441,442],{},"background-color: var(--accent)","，会跟随主题切换。",[10,445,446],{},"我第一次写的时候踩了这个坑，暗色模式下所有强调色都不变，排查了半小时。",[31,448,450],{"id":449},"静态-token-和动态-token-要分开","静态 token 和动态 token 要分开",[10,452,453],{},"不是所有 token 都需要跟随主题切换。",[455,456,457,473],"table",{},[458,459,460],"thead",{},[461,462,463,467,470],"tr",{},[464,465,466],"th",{},"类型",[464,468,469],{},"例子",[464,471,472],{},"放哪",[474,475,476,492],"tbody",{},[461,477,478,482,485],{},[479,480,481],"td",{},"动态（明暗不同）",[479,483,484],{},"颜色",[479,486,487,489,490],{},[22,488,406],{}," + ",[22,491,351],{},[461,493,494,497,500],{},[479,495,496],{},"静态（明暗相同）",[479,498,499],{},"字号、间距、圆角、动效时长",[479,501,502,503],{},"只放 ",[22,504,406],{},[10,506,507,508,510],{},"把字号也放进 ",[22,509,351],{}," 里是浪费——它不会变，重复声明只会让 CSS 变长。",[31,512,514],{"id":513},"为什么不用-css-in-js","为什么不用 CSS-in-JS",[10,516,517],{},"有人会问：为什么不直接在 JS 里算好样式？",[10,519,520],{},"三个理由：",[10,522,523,526],{},[14,524,525],{},"一、SSR 友好。"," CSS 变量在 HTML 里就能生效，不需要等 JS 执行。",[10,528,529,532],{},[14,530,531],{},"二、无运行时开销。"," 构建期生成，运行时零成本。",[10,534,535,538],{},[14,536,537],{},"三、可被浏览器优化。"," 变量切换只是重绘，不触发重排。",[31,540,541],{"id":541},"组件里怎么写",[36,543,547],{"className":544,"code":545,"language":546,"meta":44,"style":44},"language-vue shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u003Cbutton class=\"btn\">点击\u003C\u002Fbutton>\n\u003C\u002Ftemplate>\n\n\u003Cstyle scoped>\n.btn {\n  background-color: var(--accent);\n  border-radius: calc(var(--radius) \u002F 2);\n  padding: var(--space-item) var(--space-block);\n  transition: opacity var(--duration-base) var(--easing-base);\n}\n\u003C\u002Fstyle>\n","vue",[22,548,549,561,585,594,598,610,617,633,663,688,714,718],{"__ignoreMap":44},[59,550,551,554,558],{"class":61,"line":62},[59,552,553],{"class":89},"\u003C",[59,555,557],{"class":556},"s9eBZ","template",[59,559,560],{"class":89},">\n",[59,562,563,566,569,572,575,578,581,583],{"class":61,"line":69},[59,564,565],{"class":89},"  \u003C",[59,567,568],{"class":556},"button",[59,570,571],{"class":405}," class",[59,573,574],{"class":89},"=",[59,576,577],{"class":93},"\"btn\"",[59,579,580],{"class":89},">点击\u003C\u002F",[59,582,568],{"class":556},[59,584,560],{"class":89},[59,586,587,590,592],{"class":61,"line":100},[59,588,589],{"class":89},"\u003C\u002F",[59,591,557],{"class":556},[59,593,560],{"class":89},[59,595,596],{"class":61,"line":107},[59,597,104],{"emptyLinePlaceholder":103},[59,599,600,602,605,608],{"class":61,"line":120},[59,601,553],{"class":89},[59,603,604],{"class":556},"style",[59,606,607],{"class":405}," scoped",[59,609,560],{"class":89},[59,611,612,615],{"class":61,"line":126},[59,613,614],{"class":405},".btn",[59,616,308],{"class":89},[59,618,619,622,624,626,628,630],{"class":61,"line":149},[59,620,621],{"class":76},"  background-color",[59,623,317],{"class":89},[59,625,382],{"class":76},[59,627,90],{"class":89},[59,629,355],{"class":313},[59,631,632],{"class":89},");\n",[59,634,635,638,640,643,645,647,649,652,655,658,661],{"class":61,"line":173},[59,636,637],{"class":76},"  border-radius",[59,639,317],{"class":89},[59,641,642],{"class":76},"calc",[59,644,90],{"class":89},[59,646,382],{"class":76},[59,648,90],{"class":89},[59,650,651],{"class":313},"--radius",[59,653,654],{"class":89},") ",[59,656,657],{"class":72},"\u002F",[59,659,660],{"class":76}," 2",[59,662,632],{"class":89},[59,664,665,668,670,672,674,677,679,681,683,686],{"class":61,"line":188},[59,666,667],{"class":76},"  padding",[59,669,317],{"class":89},[59,671,382],{"class":76},[59,673,90],{"class":89},[59,675,676],{"class":313},"--space-item",[59,678,654],{"class":89},[59,680,382],{"class":76},[59,682,90],{"class":89},[59,684,685],{"class":313},"--space-block",[59,687,632],{"class":89},[59,689,690,693,696,698,700,703,705,707,709,712],{"class":61,"line":209},[59,691,692],{"class":76},"  transition",[59,694,695],{"class":89},": opacity ",[59,697,382],{"class":76},[59,699,90],{"class":89},[59,701,702],{"class":313},"--duration-base",[59,704,654],{"class":89},[59,706,382],{"class":76},[59,708,90],{"class":89},[59,710,711],{"class":313},"--easing-base",[59,713,632],{"class":89},[59,715,716],{"class":61,"line":215},[59,717,212],{"class":89},[59,719,720,722,724],{"class":61,"line":220},[59,721,589],{"class":89},[59,723,604],{"class":556},[59,725,560],{"class":89},[10,727,728,729,732],{},"注意这里",[14,730,731],{},"没有一个魔法数字","：",[734,735,736,745,753],"ul",{},[737,738,739,742,743],"li",{},[22,740,741],{},"var(--accent)"," 而不是 ",[22,744,320],{},[737,746,747,742,750],{},[22,748,749],{},"calc(var(--radius) \u002F 2)",[22,751,752],{},"4px",[737,754,755,742,758],{},[22,756,757],{},"var(--duration-base)",[22,759,760],{},"200ms",[31,762,763],{"id":763},"怎么验证没绕过",[10,765,766],{},"验收标准里有一条：改一个 token，全站视觉同步更新。",[10,768,769,770,732],{},"我的验证方法是",[14,771,772],{},"改一个不可能看错的值",[36,774,776],{"className":53,"code":775,"language":55,"meta":44,"style":44},"\u002F\u002F theme.config.ts\naccent: { light: '#ff0000', dark: '#00ff00' }\n",[22,777,778,783],{"__ignoreMap":44},[59,779,780],{"class":61,"line":62},[59,781,782],{"class":65},"\u002F\u002F theme.config.ts\n",[59,784,785,787,790,792,794,797,800,802,804,807],{"class":61,"line":69},[59,786,138],{"class":405},[59,788,789],{"class":89},": { ",[59,791,143],{"class":405},[59,793,317],{"class":89},[59,795,796],{"class":93},"'#ff0000'",[59,798,799],{"class":89},", ",[59,801,239],{"class":405},[59,803,317],{"class":89},[59,805,806],{"class":93},"'#00ff00'",[59,808,809],{"class":89}," }\n",[10,811,812],{},"重新构建，全站所有强调色（导航下划线、焦点环、搜索高亮、引用线）都应该变成红绿。",[10,814,815],{},"如果有任何一处没变，说明那里是硬编码的。",[31,817,818],{"id":818},"收益",[10,820,821],{},"半年下来，这套做法带来的实际好处：",[10,823,824,827],{},[14,825,826],{},"一、换肤成本 ≈ 0。"," 想试新的强调色，改两行，刷新。",[10,829,830,833,834,836],{},[14,831,832],{},"二、暗色模式不可能漏。"," 因为颜色全是变量，",[22,835,351],{}," 块里定义一次就够。",[10,838,839,842,843,845,846,849,850,17],{},[14,840,841],{},"三、设计评审有依据。"," 讨论\"这个圆角是不是太大\"时，看的是 ",[22,844,283],{}," 里的 ",[22,847,848],{},"radius: '0.5rem'","，不是某个组件里的 ",[22,851,852],{},"border-radius: 12px",[31,854,855],{"id":855},"一句话",[857,858,859],"blockquote",{},[10,860,861,862,17],{},"设计 token 的价值不在于\"有一份变量表\"，而在于",[14,863,864],{},"让绕过它变得比遵守它更麻烦",[10,866,867],{},"这才是系统。",[604,869,870],{},"html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#57606A;--shiki-dark:#9AA4B0}html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .s4XuR, html code.shiki .s4XuR{--shiki-default:#E36209;--shiki-dark:#FFAB70}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":44,"searchDepth":69,"depth":69,"links":872},[873,874,875,876,877,878,879,880],{"id":33,"depth":69,"text":34},{"id":287,"depth":69,"text":287},{"id":449,"depth":69,"text":450},{"id":513,"depth":69,"text":514},{"id":541,"depth":69,"text":541},{"id":763,"depth":69,"text":763},{"id":818,"depth":69,"text":818},{"id":855,"depth":69,"text":855},"前端",null,"2025-04-20","设计 token 写进配置文件，构建期编译成 CSS 变量，组件只读变量。这套做法让「改一处，全站生效」从口号变成了构建期保证。",false,"md",{},"\u002Fposts\u002Fcss-variables-as-api",{"title":5,"description":884},"posts\u002Fcss-variables-as-api",[892,893,894],"CSS","设计系统","构建","lFcKdgTAPyi7UONTb1lpde_0jQhwdeqRkfsn_MTj3ZU",{"prev":897,"next":882},{"path":898,"title":899,"description":900,"date":901,"updated":882,"tags":902,"category":881,"cover":905,"draft":885,"pinned":885,"featured":103},"\u002Fposts\u002Fview-transitions","View Transitions 不是动画库","用它做页面切换、做主题扩散、做列表到详情的共享元素过渡。它真正的价值不是\"让页面动起来\"，而是让你不必手动管理进入和离开两套状态。","2025-09-18",[903,904,892],"动画","浏览器","\u002Fimages\u002Fcovers\u002Fview-transition.png",[907,909,918],{"path":898,"title":899,"description":900,"date":901,"updated":882,"tags":908,"category":881,"cover":905,"draft":885,"pinned":885,"featured":103},[903,904,892],{"path":910,"title":911,"description":912,"date":913,"updated":882,"tags":914,"category":881,"cover":882,"draft":885,"pinned":885,"featured":885},"\u002Fposts\u002Fself-hosted-fonts","自托管字体的一年","从 Google Fonts 换成自托管，从自托管换成子集化分片。聊聊为什么一个中文字体能从 25MB 变成实际加载 180KB。","2025-08-08",[915,916,917],"字体","性能","中文",{"path":919,"title":920,"description":921,"date":922,"updated":882,"tags":923,"category":881,"cover":882,"draft":885,"pinned":885,"featured":885},"\u002Fposts\u002Fno-loading-feel","无加载不是速度，是连续感","一个 300ms 的白屏比一个 800ms 的骨架屏更让人焦虑。这篇讲怎么用 SSG、预取和 view transition 把页面跳转做成「一瞬间的事」。","2025-05-15",[916,924,925],"体验","Nuxt",1794113902332]