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