tailwind2-6 Exercise 3: Styling a Headless Tab List and Catching a Variant That Does Not Exist ============================================================================================= Setup: a folder with npm install tailwindcss @tailwindcss/vite vite @vitejs/plugin-react react react-dom @radix-ui/react-tabs @headlessui/react react-aria-components and the files below (I used all three libraries on one page; you need one). Build with npx vite build ; serve dist/ with any static server and open it. The scripted check (run.mjs) uses my own DevTools driver (cdp.mjs), so in practice click the second tab and inspect the elements in DevTools to read the same attributes. --- vite.config.js --- import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; export default defineConfig({ plugins: [react(), tailwindcss()], build: { outDir: 'dist' } }); --- index.html ---
--- src/style.css --- @import "tailwindcss"; --- src/main.jsx (the tab parts are the ones to copy; the dialog and disclosure were extra checks) --- import './style.css'; import { createRoot } from 'react-dom/client'; import * as RTabs from '@radix-ui/react-tabs'; import * as RDialog from '@radix-ui/react-dialog'; import { Tab, TabGroup, TabList, TabPanels, TabPanel, Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'; import { Tabs as ATabs, TabList as ATabList, Tab as ATab, TabPanel as ATabPanel, Button as AButton } from 'react-aria-components'; createRoot(document.getElementById('root')).render(
One Two A B Open dialog Hi Body Close
One Two AB Toggle Panel
One Two AB Press
); Results (attributes read from the live DOM, background from getComputedStyle): Radix tab 1 initial: aria-selected=true data-state=active bg oklch(0.623 0.214 259.815) (blue-500) Radix tab 2 initial: aria-selected=false data-state=inactive bg transparent Radix tab 1 after clicking 2: aria-selected=false data-state=inactive bg transparent Radix tab 2 after clicking 2: aria-selected=true data-state=active bg blue-500 Headless UI tab 1 initial: aria-selected=true data-headlessui-state=selected data-selected="" bg blue-500 Headless UI tab 2 initial: aria-selected=false data-headlessui-state="" bg transparent Headless UI tab 2 after click: aria-selected=true data-headlessui-state="selected hover" data-hover="" data-selected="" bg blue-500 React Aria tab 1 initial: aria-selected=true data-selected=true bg blue-500 React Aria tab 2 after click: aria-selected=true data-hovered=true data-selected=true data-focused=true bg blue-500 A variant name that does not exist (second test, a fresh Tailwind build of this one file): s.css: @import "tailwindcss"; page.html: x npx @tailwindcss/cli -i s.css -o out.css exit code 0, no message grep -n 'selected\|hovered' out.css 164: .data-hovered\:underline[data-hovered] { 167: .data-selected\:bg-blue-500[data-selected] { (no rule at all for selected:bg-blue-500 or hovered:underline) WHY THIS WORKS AS AN ANSWER --------------------------- Each library reports its state as attributes on the element, and Tailwind variants are only selectors on those attributes. Radix writes a word (data-state="active"), so the class must name the value: data-[state=active]:bg-blue-500. Headless UI and React Aria write the attribute only when the state is true (and remove it when false: tab 1 has no data-selected after the click), so the boolean shorthand data-selected:bg-blue-500, which compiles to [data-selected], is enough. The before/after rows prove the pairing works: only the selected tab has the blue background, and it moves when you click. The last test shows the failure mode when the pairing is wrong: "selected:" and "hovered:" are not Tailwind variants, the build exits 0 and writes nothing for them, and the page just looks unstyled. Check the generated CSS for one class from each library before debugging the component. (React Aria offers a separate Tailwind plugin that adds names like selected:; I did not install it.) Note: I did not test keyboard navigation (arrow keys between tabs) in any of the three; I only clicked.