[{"data":1,"prerenderedAt":1593},["ShallowReactive",2],{"page-\u002Fcomponents\u002F探究交互与样式分离的无头组件库headlessui":3},{"id":4,"title":5,"body":6,"description":12,"extension":1581,"meta":1582,"navigation":342,"path":1589,"seo":1590,"stem":1591,"__hash__":1592},"content\u002Fcomponents\u002F探究交互与样式分离的无头组件库HeadlessUI.md","探究交互与样式分离的无头组件库HeadlessUI",{"type":7,"value":8,"toc":1576},"minimark",[9,13,30,33,36,40,43,70,73,1270,1273,1293,1307,1356,1369,1379,1433,1440,1454,1457,1466,1557,1560,1563,1566,1569,1572],[10,11,12],"p",{},"样式与交互分离的思想是现代前端架构演进的一次解耦运动，我们之前使用的primevue、element ui 、ant desigen等等其实都是成品组件，它们把样式和交互死死绑在了一起。当我们需要修改部分样式的时候，就不得不使用!import来实现。",[10,14,15,16,20,21,29],{},"就算是primevue也只有一个:deep的这种方案，这可以让我们穿透作用域去修改子组件包括primevue渲染出的dom的样式，这可以实现深度定制，但是一些组件比如toast 、dialog、dropdown等组件通过appendTo属性将dom渲染到body末尾的时候，这些元素已经不在vue组件的dom子树内了，:depp就无法触及了。还有就是primevue的:pt，它通过 ",[17,18,19],"code",{},":pt=\"{ icon: { class: 'mr-2' } }\""," 这样的配置，让你直接将 Tailwind 类名或内联样式“穿透”到组件的内部特定部位（如图标）。",[22,23,24,25,28],"strong",{},"它能完美解决 ",[17,26,27],{},":deep()"," 对“传送门”组件无效的问题","。",[10,31,32],{},"但是即便是这样，也是无法完全实现交互和样式分离的，只是说这样是一个这种方案，能够让开发者深度定制样式。",[10,34,35],{},"所以，我们来谈谈一个完全符合样式与交互分离的组件库headless ui。这个是tailwindcss官方强推的组件库，很建议学习。现在是2026年，tailwind在前端的占量已经越来越大了，bootstrap存量大，但是在新项目里使用的占比已经很小了，可能是因为ai agent的原因，也是变相地推动了使用tailwnd的项目的数量持续上升，所以这个场景下我们学习和tailwind配套的headlessui还是一个比较切实际的方案。",[37,38,39],"h2",{"id":39},"headlessui本质",[10,41,42],{},"headlessui的实现本质是一个交互逻辑和视觉表现彻底分离的行为引擎，本身不渲染任何html和css样式，而是提供了一套机制来负责处理组件的状态、交互和无障碍性。",[10,44,45,46,49,50,53,54,57,58,61,62,65,66,69],{},"他用的是复合组件模式，通过上下文的传递来管理状态，最典型的就是使用 ",[17,47,48],{},"\u003CTab.Group>"," 包裹 ",[17,51,52],{},"\u003CTab.List>"," 和 ",[17,55,56],{},"\u003CTab.Panels>"," 的结构，父组件 ",[17,59,60],{},"Tab.Group"," 维护当前激活的 Tab 索引，子组件通过 Context 消费这个状态，从而实现点击 ",[17,63,64],{},"Tab"," 切换对应 ",[17,67,68],{},"Tab.Panel"," 的基础逻辑。",[10,71,72],{},"你看官方的tabs的code展示：",[74,75,80],"pre",{"className":76,"code":77,"language":78,"meta":79,"style":79},"language-vue shiki shiki-themes github-light github-dark","\u003Ctemplate>\n  \u003Cdiv class=\"w-full max-w-md px-2 py-16 sm:px-0\">\n    \u003CTabGroup>\n      \u003CTabList class=\"flex space-x-1 rounded-xl bg-blue-900\u002F20 p-1\">\n        \u003CTab\n          v-for=\"category in Object.keys(categories)\"\n          as=\"template\"\n          :key=\"category\"\n          v-slot=\"{ selected }\"\n        >\n          \u003Cbutton\n            :class=\"[\n              'w-full rounded-lg py-2.5 text-sm font-medium leading-5',\n              'ring-white\u002F60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2',\n              selected\n                ? 'bg-white text-blue-700 shadow'\n                : 'text-blue-100 hover:bg-white\u002F[0.12] hover:text-white',\n            ]\"\n          >\n            {{ category }}\n          \u003C\u002Fbutton>\n        \u003C\u002FTab>\n      \u003C\u002FTabList>\n\n      \u003CTabPanels class=\"mt-2\">\n        \u003CTabPanel\n          v-for=\"(posts, idx) in Object.values(categories)\"\n          :key=\"idx\"\n          :class=\"[\n            'rounded-xl bg-white p-3',\n            'ring-white\u002F60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2',\n          ]\"\n        >\n          \u003Cul>\n            \u003Cli\n              v-for=\"post in posts\"\n              :key=\"post.id\"\n              class=\"relative rounded-md p-3 hover:bg-gray-100\"\n            >\n              \u003Ch3 class=\"text-sm font-medium leading-5\">\n                {{ post.title }}\n              \u003C\u002Fh3>\n\n              \u003Cul\n                class=\"mt-1 flex space-x-1 text-xs font-normal leading-4 text-gray-500\"\n              >\n                \u003Cli>{{ post.date }}\u003C\u002Fli>\n                \u003Cli>&middot;\u003C\u002Fli>\n                \u003Cli>{{ post.commentCount }} comments\u003C\u002Fli>\n                \u003Cli>&middot;\u003C\u002Fli>\n                \u003Cli>{{ post.shareCount }} shares\u003C\u002Fli>\n              \u003C\u002Ful>\n\n              \u003Ca\n                href=\"#\"\n                :class=\"[\n                  'absolute inset-0 rounded-md',\n                  'ring-blue-400 focus:z-10 focus:outline-none focus:ring-2',\n                ]\"\n              \u002F>\n            \u003C\u002Fli>\n          \u003C\u002Ful>\n        \u003C\u002FTabPanel>\n      \u003C\u002FTabPanels>\n    \u003C\u002FTabGroup>\n  \u003C\u002Fdiv>\n\u003C\u002Ftemplate>\n\n\u003Cscript setup>\nimport { ref } from 'vue'\nimport { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui\u002Fvue'\n\nconst categories = ref({\n  Recent: [\n    {\n      id: 1,\n      title: 'Does drinking coffee make you smarter?',\n      date: '5h ago',\n      commentCount: 5,\n      shareCount: 2,\n    },\n    {\n      id: 2,\n      title: \"So you've bought coffee... now what?\",\n      date: '2h ago',\n      commentCount: 3,\n      shareCount: 2,\n    },\n  ],\n  Popular: [\n    {\n      id: 1,\n      title: 'Is tech making coffee better or worse?',\n      date: 'Jan 7',\n      commentCount: 29,\n      shareCount: 16,\n    },\n    {\n      id: 2,\n      title: 'The most innovative things happening in coffee',\n      date: 'Mar 19',\n      commentCount: 24,\n      shareCount: 12,\n    },\n  ],\n  Trending: [\n    {\n      id: 1,\n      title: 'Ask Me Anything: 10 answers to your questions about coffee',\n      date: '2d ago',\n      commentCount: 9,\n      shareCount: 5,\n    },\n    {\n      id: 2,\n      title: \"The worst advice we've ever heard about coffee\",\n      date: '4d ago',\n      commentCount: 1,\n      shareCount: 2,\n    },\n  ],\n})\n\u003C\u002Fscript>\n","vue","",[17,81,82,98,120,131,149,158,188,199,217,232,238,247,258,264,270,276,282,288,294,300,306,317,327,337,344,361,369,392,408,423,432,440,448,453,463,472,483,494,505,511,529,535,545,550,558,569,575,591,612,626,643,657,666,671,679,690,700,706,712,718,727,737,746,756,765,775,785,794,799,812,827,840,845,863,869,875,886,897,908,919,930,936,941,950,960,970,980,989,994,1000,1006,1011,1020,1030,1040,1050,1060,1065,1070,1079,1089,1099,1109,1119,1124,1129,1135,1140,1149,1159,1169,1179,1188,1193,1198,1207,1217,1227,1236,1245,1250,1255,1261],{"__ignoreMap":79},[83,84,87,91,95],"span",{"class":85,"line":86},"line",1,[83,88,90],{"class":89},"sVt8B","\u003C",[83,92,94],{"class":93},"s9eBZ","template",[83,96,97],{"class":89},">\n",[83,99,101,104,107,111,114,118],{"class":85,"line":100},2,[83,102,103],{"class":89},"  \u003C",[83,105,106],{"class":93},"div",[83,108,110],{"class":109},"sScJk"," class",[83,112,113],{"class":89},"=",[83,115,117],{"class":116},"sZZnC","\"w-full max-w-md px-2 py-16 sm:px-0\"",[83,119,97],{"class":89},[83,121,123,126,129],{"class":85,"line":122},3,[83,124,125],{"class":89},"    \u003C",[83,127,128],{"class":93},"TabGroup",[83,130,97],{"class":89},[83,132,134,137,140,142,144,147],{"class":85,"line":133},4,[83,135,136],{"class":89},"      \u003C",[83,138,139],{"class":93},"TabList",[83,141,110],{"class":109},[83,143,113],{"class":89},[83,145,146],{"class":116},"\"flex space-x-1 rounded-xl bg-blue-900\u002F20 p-1\"",[83,148,97],{"class":89},[83,150,152,155],{"class":85,"line":151},5,[83,153,154],{"class":89},"        \u003C",[83,156,157],{"class":93},"Tab\n",[83,159,161,165,167,170,173,176,179,182,185],{"class":85,"line":160},6,[83,162,164],{"class":163},"szBVR","          v-for",[83,166,113],{"class":89},[83,168,169],{"class":116},"\"",[83,171,172],{"class":89},"category ",[83,174,175],{"class":163},"in",[83,177,178],{"class":89}," Object.",[83,180,181],{"class":109},"keys",[83,183,184],{"class":89},"(categories)",[83,186,187],{"class":116},"\"\n",[83,189,191,194,196],{"class":85,"line":190},7,[83,192,193],{"class":109},"          as",[83,195,113],{"class":89},[83,197,198],{"class":116},"\"template\"\n",[83,200,202,205,208,210,212,215],{"class":85,"line":201},8,[83,203,204],{"class":89},"          :",[83,206,207],{"class":109},"key",[83,209,113],{"class":89},[83,211,169],{"class":116},[83,213,214],{"class":89},"category",[83,216,187],{"class":116},[83,218,220,223,225,227,230],{"class":85,"line":219},9,[83,221,222],{"class":109},"          v-slot",[83,224,113],{"class":89},[83,226,169],{"class":116},[83,228,229],{"class":89},"{ selected }",[83,231,187],{"class":116},[83,233,235],{"class":85,"line":234},10,[83,236,237],{"class":89},"        >\n",[83,239,241,244],{"class":85,"line":240},11,[83,242,243],{"class":89},"          \u003C",[83,245,246],{"class":93},"button\n",[83,248,250,253,255],{"class":85,"line":249},12,[83,251,252],{"class":109},"            :class",[83,254,113],{"class":89},[83,256,257],{"class":116},"\"[\n",[83,259,261],{"class":85,"line":260},13,[83,262,263],{"class":116},"              'w-full rounded-lg py-2.5 text-sm font-medium leading-5',\n",[83,265,267],{"class":85,"line":266},14,[83,268,269],{"class":116},"              'ring-white\u002F60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2',\n",[83,271,273],{"class":85,"line":272},15,[83,274,275],{"class":116},"              selected\n",[83,277,279],{"class":85,"line":278},16,[83,280,281],{"class":116},"                ? 'bg-white text-blue-700 shadow'\n",[83,283,285],{"class":85,"line":284},17,[83,286,287],{"class":116},"                : 'text-blue-100 hover:bg-white\u002F[0.12] hover:text-white',\n",[83,289,291],{"class":85,"line":290},18,[83,292,293],{"class":116},"            ]\"\n",[83,295,297],{"class":85,"line":296},19,[83,298,299],{"class":89},"          >\n",[83,301,303],{"class":85,"line":302},20,[83,304,305],{"class":89},"            {{ category }}\n",[83,307,309,312,315],{"class":85,"line":308},21,[83,310,311],{"class":89},"          \u003C\u002F",[83,313,314],{"class":93},"button",[83,316,97],{"class":89},[83,318,320,323,325],{"class":85,"line":319},22,[83,321,322],{"class":89},"        \u003C\u002F",[83,324,64],{"class":93},[83,326,97],{"class":89},[83,328,330,333,335],{"class":85,"line":329},23,[83,331,332],{"class":89},"      \u003C\u002F",[83,334,139],{"class":93},[83,336,97],{"class":89},[83,338,340],{"class":85,"line":339},24,[83,341,343],{"emptyLinePlaceholder":342},true,"\n",[83,345,347,349,352,354,356,359],{"class":85,"line":346},25,[83,348,136],{"class":89},[83,350,351],{"class":93},"TabPanels",[83,353,110],{"class":109},[83,355,113],{"class":89},[83,357,358],{"class":116},"\"mt-2\"",[83,360,97],{"class":89},[83,362,364,366],{"class":85,"line":363},26,[83,365,154],{"class":89},[83,367,368],{"class":93},"TabPanel\n",[83,370,372,374,376,378,381,383,385,388,390],{"class":85,"line":371},27,[83,373,164],{"class":163},[83,375,113],{"class":89},[83,377,169],{"class":116},[83,379,380],{"class":89},"(posts, idx) ",[83,382,175],{"class":163},[83,384,178],{"class":89},[83,386,387],{"class":109},"values",[83,389,184],{"class":89},[83,391,187],{"class":116},[83,393,395,397,399,401,403,406],{"class":85,"line":394},28,[83,396,204],{"class":89},[83,398,207],{"class":109},[83,400,113],{"class":89},[83,402,169],{"class":116},[83,404,405],{"class":89},"idx",[83,407,187],{"class":116},[83,409,411,413,416,418,420],{"class":85,"line":410},29,[83,412,204],{"class":89},[83,414,415],{"class":109},"class",[83,417,113],{"class":89},[83,419,169],{"class":116},[83,421,422],{"class":89},"[\n",[83,424,426,429],{"class":85,"line":425},30,[83,427,428],{"class":116},"            'rounded-xl bg-white p-3'",[83,430,431],{"class":89},",\n",[83,433,435,438],{"class":85,"line":434},31,[83,436,437],{"class":116},"            'ring-white\u002F60 ring-offset-2 ring-offset-blue-400 focus:outline-none focus:ring-2'",[83,439,431],{"class":89},[83,441,443,446],{"class":85,"line":442},32,[83,444,445],{"class":89},"          ]",[83,447,187],{"class":116},[83,449,451],{"class":85,"line":450},33,[83,452,237],{"class":89},[83,454,456,458,461],{"class":85,"line":455},34,[83,457,243],{"class":89},[83,459,460],{"class":93},"ul",[83,462,97],{"class":89},[83,464,466,469],{"class":85,"line":465},35,[83,467,468],{"class":89},"            \u003C",[83,470,471],{"class":93},"li\n",[83,473,475,478,480],{"class":85,"line":474},36,[83,476,477],{"class":109},"              v-for",[83,479,113],{"class":89},[83,481,482],{"class":116},"\"post in posts\"\n",[83,484,486,489,491],{"class":85,"line":485},37,[83,487,488],{"class":109},"              :key",[83,490,113],{"class":89},[83,492,493],{"class":116},"\"post.id\"\n",[83,495,497,500,502],{"class":85,"line":496},38,[83,498,499],{"class":109},"              class",[83,501,113],{"class":89},[83,503,504],{"class":116},"\"relative rounded-md p-3 hover:bg-gray-100\"\n",[83,506,508],{"class":85,"line":507},39,[83,509,510],{"class":89},"            >\n",[83,512,514,517,520,522,524,527],{"class":85,"line":513},40,[83,515,516],{"class":89},"              \u003C",[83,518,519],{"class":93},"h3",[83,521,110],{"class":109},[83,523,113],{"class":89},[83,525,526],{"class":116},"\"text-sm font-medium leading-5\"",[83,528,97],{"class":89},[83,530,532],{"class":85,"line":531},41,[83,533,534],{"class":89},"                {{ post.title }}\n",[83,536,538,541,543],{"class":85,"line":537},42,[83,539,540],{"class":89},"              \u003C\u002F",[83,542,519],{"class":93},[83,544,97],{"class":89},[83,546,548],{"class":85,"line":547},43,[83,549,343],{"emptyLinePlaceholder":342},[83,551,553,555],{"class":85,"line":552},44,[83,554,516],{"class":89},[83,556,557],{"class":93},"ul\n",[83,559,561,564,566],{"class":85,"line":560},45,[83,562,563],{"class":109},"                class",[83,565,113],{"class":89},[83,567,568],{"class":116},"\"mt-1 flex space-x-1 text-xs font-normal leading-4 text-gray-500\"\n",[83,570,572],{"class":85,"line":571},46,[83,573,574],{"class":89},"              >\n",[83,576,578,581,584,587,589],{"class":85,"line":577},47,[83,579,580],{"class":89},"                \u003C",[83,582,583],{"class":93},"li",[83,585,586],{"class":89},">{{ post.date }}\u003C\u002F",[83,588,583],{"class":93},[83,590,97],{"class":89},[83,592,594,596,598,601,605,608,610],{"class":85,"line":593},48,[83,595,580],{"class":89},[83,597,583],{"class":93},[83,599,600],{"class":89},">",[83,602,604],{"class":603},"sj4cs","&middot;",[83,606,607],{"class":89},"\u003C\u002F",[83,609,583],{"class":93},[83,611,97],{"class":89},[83,613,615,617,619,622,624],{"class":85,"line":614},49,[83,616,580],{"class":89},[83,618,583],{"class":93},[83,620,621],{"class":89},">{{ post.commentCount }} comments\u003C\u002F",[83,623,583],{"class":93},[83,625,97],{"class":89},[83,627,629,631,633,635,637,639,641],{"class":85,"line":628},50,[83,630,580],{"class":89},[83,632,583],{"class":93},[83,634,600],{"class":89},[83,636,604],{"class":603},[83,638,607],{"class":89},[83,640,583],{"class":93},[83,642,97],{"class":89},[83,644,646,648,650,653,655],{"class":85,"line":645},51,[83,647,580],{"class":89},[83,649,583],{"class":93},[83,651,652],{"class":89},">{{ post.shareCount }} shares\u003C\u002F",[83,654,583],{"class":93},[83,656,97],{"class":89},[83,658,660,662,664],{"class":85,"line":659},52,[83,661,540],{"class":89},[83,663,460],{"class":93},[83,665,97],{"class":89},[83,667,669],{"class":85,"line":668},53,[83,670,343],{"emptyLinePlaceholder":342},[83,672,674,676],{"class":85,"line":673},54,[83,675,516],{"class":89},[83,677,678],{"class":93},"a\n",[83,680,682,685,687],{"class":85,"line":681},55,[83,683,684],{"class":109},"                href",[83,686,113],{"class":89},[83,688,689],{"class":116},"\"#\"\n",[83,691,693,696,698],{"class":85,"line":692},56,[83,694,695],{"class":109},"                :class",[83,697,113],{"class":89},[83,699,257],{"class":116},[83,701,703],{"class":85,"line":702},57,[83,704,705],{"class":116},"                  'absolute inset-0 rounded-md',\n",[83,707,709],{"class":85,"line":708},58,[83,710,711],{"class":116},"                  'ring-blue-400 focus:z-10 focus:outline-none focus:ring-2',\n",[83,713,715],{"class":85,"line":714},59,[83,716,717],{"class":116},"                ]\"\n",[83,719,721,725],{"class":85,"line":720},60,[83,722,724],{"class":723},"s7hpK","              \u002F",[83,726,97],{"class":89},[83,728,730,733,735],{"class":85,"line":729},61,[83,731,732],{"class":89},"            \u003C\u002F",[83,734,583],{"class":93},[83,736,97],{"class":89},[83,738,740,742,744],{"class":85,"line":739},62,[83,741,311],{"class":89},[83,743,460],{"class":93},[83,745,97],{"class":89},[83,747,749,751,754],{"class":85,"line":748},63,[83,750,322],{"class":89},[83,752,753],{"class":93},"TabPanel",[83,755,97],{"class":89},[83,757,759,761,763],{"class":85,"line":758},64,[83,760,332],{"class":89},[83,762,351],{"class":93},[83,764,97],{"class":89},[83,766,768,771,773],{"class":85,"line":767},65,[83,769,770],{"class":89},"    \u003C\u002F",[83,772,128],{"class":93},[83,774,97],{"class":89},[83,776,778,781,783],{"class":85,"line":777},66,[83,779,780],{"class":89},"  \u003C\u002F",[83,782,106],{"class":93},[83,784,97],{"class":89},[83,786,788,790,792],{"class":85,"line":787},67,[83,789,607],{"class":89},[83,791,94],{"class":93},[83,793,97],{"class":89},[83,795,797],{"class":85,"line":796},68,[83,798,343],{"emptyLinePlaceholder":342},[83,800,802,804,807,810],{"class":85,"line":801},69,[83,803,90],{"class":89},[83,805,806],{"class":93},"script",[83,808,809],{"class":109}," setup",[83,811,97],{"class":89},[83,813,815,818,821,824],{"class":85,"line":814},70,[83,816,817],{"class":163},"import",[83,819,820],{"class":89}," { ref } ",[83,822,823],{"class":163},"from",[83,825,826],{"class":116}," 'vue'\n",[83,828,830,832,835,837],{"class":85,"line":829},71,[83,831,817],{"class":163},[83,833,834],{"class":89}," { TabGroup, TabList, Tab, TabPanels, TabPanel } ",[83,836,823],{"class":163},[83,838,839],{"class":116}," '@headlessui\u002Fvue'\n",[83,841,843],{"class":85,"line":842},72,[83,844,343],{"emptyLinePlaceholder":342},[83,846,848,851,854,857,860],{"class":85,"line":847},73,[83,849,850],{"class":163},"const",[83,852,853],{"class":603}," categories",[83,855,856],{"class":163}," =",[83,858,859],{"class":109}," ref",[83,861,862],{"class":89},"({\n",[83,864,866],{"class":85,"line":865},74,[83,867,868],{"class":89},"  Recent: [\n",[83,870,872],{"class":85,"line":871},75,[83,873,874],{"class":89},"    {\n",[83,876,878,881,884],{"class":85,"line":877},76,[83,879,880],{"class":89},"      id: ",[83,882,883],{"class":603},"1",[83,885,431],{"class":89},[83,887,889,892,895],{"class":85,"line":888},77,[83,890,891],{"class":89},"      title: ",[83,893,894],{"class":116},"'Does drinking coffee make you smarter?'",[83,896,431],{"class":89},[83,898,900,903,906],{"class":85,"line":899},78,[83,901,902],{"class":89},"      date: ",[83,904,905],{"class":116},"'5h ago'",[83,907,431],{"class":89},[83,909,911,914,917],{"class":85,"line":910},79,[83,912,913],{"class":89},"      commentCount: ",[83,915,916],{"class":603},"5",[83,918,431],{"class":89},[83,920,922,925,928],{"class":85,"line":921},80,[83,923,924],{"class":89},"      shareCount: ",[83,926,927],{"class":603},"2",[83,929,431],{"class":89},[83,931,933],{"class":85,"line":932},81,[83,934,935],{"class":89},"    },\n",[83,937,939],{"class":85,"line":938},82,[83,940,874],{"class":89},[83,942,944,946,948],{"class":85,"line":943},83,[83,945,880],{"class":89},[83,947,927],{"class":603},[83,949,431],{"class":89},[83,951,953,955,958],{"class":85,"line":952},84,[83,954,891],{"class":89},[83,956,957],{"class":116},"\"So you've bought coffee... now what?\"",[83,959,431],{"class":89},[83,961,963,965,968],{"class":85,"line":962},85,[83,964,902],{"class":89},[83,966,967],{"class":116},"'2h ago'",[83,969,431],{"class":89},[83,971,973,975,978],{"class":85,"line":972},86,[83,974,913],{"class":89},[83,976,977],{"class":603},"3",[83,979,431],{"class":89},[83,981,983,985,987],{"class":85,"line":982},87,[83,984,924],{"class":89},[83,986,927],{"class":603},[83,988,431],{"class":89},[83,990,992],{"class":85,"line":991},88,[83,993,935],{"class":89},[83,995,997],{"class":85,"line":996},89,[83,998,999],{"class":89},"  ],\n",[83,1001,1003],{"class":85,"line":1002},90,[83,1004,1005],{"class":89},"  Popular: [\n",[83,1007,1009],{"class":85,"line":1008},91,[83,1010,874],{"class":89},[83,1012,1014,1016,1018],{"class":85,"line":1013},92,[83,1015,880],{"class":89},[83,1017,883],{"class":603},[83,1019,431],{"class":89},[83,1021,1023,1025,1028],{"class":85,"line":1022},93,[83,1024,891],{"class":89},[83,1026,1027],{"class":116},"'Is tech making coffee better or worse?'",[83,1029,431],{"class":89},[83,1031,1033,1035,1038],{"class":85,"line":1032},94,[83,1034,902],{"class":89},[83,1036,1037],{"class":116},"'Jan 7'",[83,1039,431],{"class":89},[83,1041,1043,1045,1048],{"class":85,"line":1042},95,[83,1044,913],{"class":89},[83,1046,1047],{"class":603},"29",[83,1049,431],{"class":89},[83,1051,1053,1055,1058],{"class":85,"line":1052},96,[83,1054,924],{"class":89},[83,1056,1057],{"class":603},"16",[83,1059,431],{"class":89},[83,1061,1063],{"class":85,"line":1062},97,[83,1064,935],{"class":89},[83,1066,1068],{"class":85,"line":1067},98,[83,1069,874],{"class":89},[83,1071,1073,1075,1077],{"class":85,"line":1072},99,[83,1074,880],{"class":89},[83,1076,927],{"class":603},[83,1078,431],{"class":89},[83,1080,1082,1084,1087],{"class":85,"line":1081},100,[83,1083,891],{"class":89},[83,1085,1086],{"class":116},"'The most innovative things happening in coffee'",[83,1088,431],{"class":89},[83,1090,1092,1094,1097],{"class":85,"line":1091},101,[83,1093,902],{"class":89},[83,1095,1096],{"class":116},"'Mar 19'",[83,1098,431],{"class":89},[83,1100,1102,1104,1107],{"class":85,"line":1101},102,[83,1103,913],{"class":89},[83,1105,1106],{"class":603},"24",[83,1108,431],{"class":89},[83,1110,1112,1114,1117],{"class":85,"line":1111},103,[83,1113,924],{"class":89},[83,1115,1116],{"class":603},"12",[83,1118,431],{"class":89},[83,1120,1122],{"class":85,"line":1121},104,[83,1123,935],{"class":89},[83,1125,1127],{"class":85,"line":1126},105,[83,1128,999],{"class":89},[83,1130,1132],{"class":85,"line":1131},106,[83,1133,1134],{"class":89},"  Trending: [\n",[83,1136,1138],{"class":85,"line":1137},107,[83,1139,874],{"class":89},[83,1141,1143,1145,1147],{"class":85,"line":1142},108,[83,1144,880],{"class":89},[83,1146,883],{"class":603},[83,1148,431],{"class":89},[83,1150,1152,1154,1157],{"class":85,"line":1151},109,[83,1153,891],{"class":89},[83,1155,1156],{"class":116},"'Ask Me Anything: 10 answers to your questions about coffee'",[83,1158,431],{"class":89},[83,1160,1162,1164,1167],{"class":85,"line":1161},110,[83,1163,902],{"class":89},[83,1165,1166],{"class":116},"'2d ago'",[83,1168,431],{"class":89},[83,1170,1172,1174,1177],{"class":85,"line":1171},111,[83,1173,913],{"class":89},[83,1175,1176],{"class":603},"9",[83,1178,431],{"class":89},[83,1180,1182,1184,1186],{"class":85,"line":1181},112,[83,1183,924],{"class":89},[83,1185,916],{"class":603},[83,1187,431],{"class":89},[83,1189,1191],{"class":85,"line":1190},113,[83,1192,935],{"class":89},[83,1194,1196],{"class":85,"line":1195},114,[83,1197,874],{"class":89},[83,1199,1201,1203,1205],{"class":85,"line":1200},115,[83,1202,880],{"class":89},[83,1204,927],{"class":603},[83,1206,431],{"class":89},[83,1208,1210,1212,1215],{"class":85,"line":1209},116,[83,1211,891],{"class":89},[83,1213,1214],{"class":116},"\"The worst advice we've ever heard about coffee\"",[83,1216,431],{"class":89},[83,1218,1220,1222,1225],{"class":85,"line":1219},117,[83,1221,902],{"class":89},[83,1223,1224],{"class":116},"'4d ago'",[83,1226,431],{"class":89},[83,1228,1230,1232,1234],{"class":85,"line":1229},118,[83,1231,913],{"class":89},[83,1233,883],{"class":603},[83,1235,431],{"class":89},[83,1237,1239,1241,1243],{"class":85,"line":1238},119,[83,1240,924],{"class":89},[83,1242,927],{"class":603},[83,1244,431],{"class":89},[83,1246,1248],{"class":85,"line":1247},120,[83,1249,935],{"class":89},[83,1251,1253],{"class":85,"line":1252},121,[83,1254,999],{"class":89},[83,1256,1258],{"class":85,"line":1257},122,[83,1259,1260],{"class":89},"})\n",[83,1262,1264,1266,1268],{"class":85,"line":1263},123,[83,1265,607],{"class":89},[83,1267,806],{"class":93},[83,1269,97],{"class":89},[10,1271,1272],{},"TapGroup是最外层，负责维护哪个Tab被选中，TabList和Tab负责渲染按钮，TabPanels和TabPanel负责渲染对应的内容区域，它们之间通过Vue的provide\u002Finject共享状态，你不需要手动编写任何v-model来绑定高亮逻辑。",[10,1274,1275,1276,1281,1282,1285,1286,1289,1290,1292],{},"里面使用了 ",[22,1277,1278],{},[17,1279,1280],{},"as=\"template\"","：让 Headless UI：“不参与渲染任何多余的 ",[17,1283,1284],{},"\u003Cdiv>"," 或 ",[17,1287,1288],{},"\u003Cbutton>"," 标签，只需要提供逻辑”。然后它把渲染控制权交还给开发者（我们在里面写了原生的 ",[17,1291,1288],{},"）。",[10,1294,1295,1298,1299,1302,1303,1306],{},[17,1296,1297],{},"v-slot=\"{ selected }\"","：这就是 ",[22,1300,1301],{},"Render Props","。Headless UI 在底层算出了当前这个 Tab 到底有没有被选中（",[17,1304,1305],{},"selected"," 是 true 还是 false），把它暴露给你。它只提供状态，至于选中了是变蓝还是加粗，它完全不管。下面就可以看到selected的参与，随后我们用tailwind来接管样式：",[74,1308,1312],{"className":1309,"code":1310,"language":1311,"meta":79,"style":79},"language-ts shiki shiki-themes github-light github-dark",":class=\"[\n  'w-full rounded-lg py-2.5 ...',\n  selected ? 'bg-white text-blue-700 shadow' : 'text-blue-100 hover:bg-white\u002F[0.12]'\n]\"\n","ts",[17,1313,1314,1325,1332,1349],{"__ignoreMap":79},[83,1315,1316,1319,1321,1323],{"class":85,"line":86},[83,1317,1318],{"class":89},":class",[83,1320,113],{"class":163},[83,1322,169],{"class":116},[83,1324,422],{"class":723},[83,1326,1327,1330],{"class":85,"line":100},[83,1328,1329],{"class":116},"  'w-full rounded-lg py-2.5 ...'",[83,1331,431],{"class":89},[83,1333,1334,1337,1340,1343,1346],{"class":85,"line":122},[83,1335,1336],{"class":89},"  selected ",[83,1338,1339],{"class":163},"?",[83,1341,1342],{"class":116}," 'bg-white text-blue-700 shadow'",[83,1344,1345],{"class":163}," :",[83,1347,1348],{"class":116}," 'text-blue-100 hover:bg-white\u002F[0.12]'\n",[83,1350,1351,1354],{"class":85,"line":133},[83,1352,1353],{"class":89},"]",[83,1355,187],{"class":116},[10,1357,1358,1359,1285,1362,1365,1366,1368],{},"Headless UI 没有给你提供任何 ",[17,1360,1361],{},".tab-active",[17,1363,1364],{},".tab-inactive"," 这种自带的 CSS 类名。所有的视觉效果，完全是由你通过 Tailwind 驱动的 ",[17,1367,1305],{}," 布尔值拼接出来的。",[10,1370,1371,1372,1374,1375,1378],{},"这段代码使用了 ",[17,1373,1297],{},"，这是 Headless UI ",[22,1376,1377],{},"v1 版本","的经典写法（Render Props）。如果是v2版本的话，官方写法会比较简洁，我们会使用data属性来完成，类似下面：",[74,1380,1384],{"className":1381,"code":1382,"language":1383,"meta":79,"style":79},"language-html shiki shiki-themes github-light github-dark","\u003CTab v-for=\"category in Object.keys(categories)\" :key=\"category\"\n     class=\"w-full rounded-lg py-2.5 ... data-[selected]:bg-white data-[selected]:text-blue-700 data-[selected]:shadow\">\n  {{ category }}\n\u003C\u002FTab>\n","html",[17,1385,1386,1408,1420,1425],{"__ignoreMap":79},[83,1387,1388,1390,1392,1395,1397,1400,1403,1405],{"class":85,"line":86},[83,1389,90],{"class":89},[83,1391,64],{"class":723},[83,1393,1394],{"class":109}," v-for",[83,1396,113],{"class":89},[83,1398,1399],{"class":116},"\"category in Object.keys(categories)\"",[83,1401,1402],{"class":109}," :key",[83,1404,113],{"class":89},[83,1406,1407],{"class":116},"\"category\"\n",[83,1409,1410,1413,1415,1418],{"class":85,"line":100},[83,1411,1412],{"class":109},"     class",[83,1414,113],{"class":89},[83,1416,1417],{"class":116},"\"w-full rounded-lg py-2.5 ... data-[selected]:bg-white data-[selected]:text-blue-700 data-[selected]:shadow\"",[83,1419,97],{"class":89},[83,1421,1422],{"class":85,"line":122},[83,1423,1424],{"class":89},"  {{ category }}\n",[83,1426,1427,1429,1431],{"class":85,"line":133},[83,1428,607],{"class":89},[83,1430,64],{"class":723},[83,1432,97],{"class":89},[10,1434,1435,1436,1439],{},"现在最尴尬的事情是headlessui目前生态不对称，截止到2026\u002F10\u002F26，react版本已经迭代到了2.2版本，但是vue版本现在依旧停留在了1.7，这种版本差异是有明确的技术和规划原因的。由 ",[22,1437,1438],{},"Tailwind Labs 团队的资源优先级","决定的，核心团队的精力投入到了tailwindcss v4的研发上，headlessui的作者adam wathan表示团队年内没有明确的vue v2开发路线图，因为安排已经满了.....",[10,1441,1442,1443,53,1446,1449,1450,1453],{},"此外，React v2 底层依赖了 ",[17,1444,1445],{},"@react-aria\u002Ffocus",[17,1447,1448],{},"@react-aria\u002Finteractions"," 这些来自 Adobe 的库来处理焦点和交互。在 Vue 生态中，",[22,1451,1452],{},"可能还没有完全对等的替代方案","，这也构成了技术迁移上的一个实际障碍。",[37,1455,1456],{"id":1456},"v2的更新",[10,1458,1459,1460],{},"React v2 是一次重大的架构重写，带来了许多 v1.7（Vue版）不具备的现代化特性：",[1461,1462],"a",{"href":1463,"rel":1464},"https:\u002F\u002Fillegalscreed.cn\u002Fzh\u002Ffrontend-framework\u002Fcomponents\u002Fheadless-ui\u002F",[1465],"nofollow",[460,1467,1468,1494,1507,1531],{},[583,1469,1470,1473,1474,1477,1478,1481,1482,1485,1486,1489,1490,1493],{},[22,1471,1472],{},"从 Render Props 转向 Data 属性","：这是最核心的体验提升。v2 不再需要 ",[17,1475,1476],{},"v-slot"," 来获取状态，而是直接在 DOM 上暴露 ",[17,1479,1480],{},"data-open","、",[17,1483,1484],{},"data-selected"," 等属性，样式完全由 CSS（如 ",[17,1487,1488],{},"data-[selected]:","）驱动，",[22,1491,1492],{},"不会触发不必要的组件重渲染","，性能更优。",[583,1495,1496,1499,1500,1503,1504],{},[22,1497,1498],{},"内置定位引擎","：v2 集成了 ",[17,1501,1502],{},"anchor"," 属性（底层基于 Floating UI），用于处理下拉菜单、弹出层的自动翻转和边界碰撞检测，无需再手动计算位置。",[1461,1505],{"href":1463,"rel":1506},[1465],[583,1508,1509,1512,1513,1516,1517,1481,1520,1481,1523,1526,1527],{},[22,1510,1511],{},"简化的过渡动画","：v2.1 引入了全新的 ",[17,1514,1515],{},"transition"," 属性，结合 ",[17,1518,1519],{},"data-closed",[17,1521,1522],{},"data-enter",[17,1524,1525],{},"data-leave"," 等阶段属性，只需添加几个 CSS 类就能实现复杂的进出场动画。",[1461,1528],{"href":1529,"rel":1530},"https:\u002F\u002Fwww.tailwindcss.cn\u002Fblog\u002F2024-06-21-headless-ui-v2-1",[1465],[583,1532,1533,1536,1537,1481,1540,1481,1543,1481,1546,1549,1550,1481,1553,1556],{},[22,1534,1535],{},"更丰富的表单组件","：v2 新增了 ",[17,1538,1539],{},"Checkbox",[17,1541,1542],{},"Input",[17,1544,1545],{},"Select",[17,1547,1548],{},"Textarea"," 等一系列原生表单组件，并自动处理 ",[17,1551,1552],{},"aria-describedby",[17,1554,1555],{},"id"," 等无障碍关联，扩展了组件库的适用范围。",[37,1558,1559],{"id":1559},"其余类似的库",[10,1561,1562],{},"例如Ark UI，支持vue，基于状态机，功能比较强大，或者Primevue的unstyled模式，它们的设计理念和headlessui v2比较接近。",[10,1564,1565],{},"AruUI核心思路和headless UI类似，但是依赖的基础库更加重，也更加灵活和强大。它基于zag.js状态机，arkui的交互由zag.js驱动，这是框架无关的有限状态机库，所以组件的所有状态比如打开\u002F关闭、选中\u002F未选中和状态抓换都由一个独立的、可预测的状态机管理，而不是散落在组件内部，这带来了极高的测试和逻辑上的严谨。",[10,1567,1568],{},"并且得益于状态机的抽象层，arkui可以给react、vue、solid、svelte提供完全相同的api和交互行为，如果开发团队涉及到了多个框架，那么Ark UI是可以极大降级维护成本的。生产级别的组件有45+个，功能覆盖面很广，包括headlessUI所有没有的data picker、color picker、carousel等复杂组件，ark ui 本身是chakra ui v3的底层基础，定位非常清晰。",[10,1570,1571],{},"primevue unstyled模式是让原本功能齐全的primevue组件卸载css样式，如果你安装的时候通过unstyled: true来全局开启，或者可以给单个组件添加unstyled属性来局部启用。pass through属性是unstyled模式下的关键，组件本身由于不包含像是，但是可以通过:pt来向组件内部传递特定DOM节点，比如root label icon传递class style或者事件处理器。",[1573,1574,1575],"style",{},"html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}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 .s7hpK, html code.shiki .s7hpK{--shiki-default:#B31D28;--shiki-default-font-style:italic;--shiki-dark:#FDAEB7;--shiki-dark-font-style:italic}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);}",{"title":79,"searchDepth":100,"depth":100,"links":1577},[1578,1579,1580],{"id":39,"depth":100,"text":39},{"id":1456,"depth":100,"text":1456},{"id":1559,"depth":100,"text":1559},"md",{"date":1583,"tags":1584},"2026-12-07",[1585,1586,1587,78,1588],"headlessui","components","css","react","\u002Fcomponents\u002F探究交互与样式分离的无头组件库headlessui",{"description":12},"components\u002F探究交互与样式分离的无头组件库HeadlessUI","kLng-8zJD8_Lv1H3pmysvySBWacQUBgr92O1uxFw8GM",1793064663296]