Last active
June 17, 2025 18:24
-
-
Save Neophen/2f512ace1e7182e5346076333e4a0fdc to your computer and use it in GitHub Desktop.
Better states, plugin for Tailwind v4
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <div class="grid gap-6 bg-slate-100 p-4"> | |
| <div class="grid gap-4 rounded-md border bg-white p-4 pressed-action:text-white"> | |
| <div class="group peer grid gap-2 rounded-md border p-4 hovered-action:bg-blue-100 pressed-action:bg-blue-500"> | |
| <h2 class="font-bold">group - peer - group-hovered-action</h2> | |
| <button type="button" class="action block rounded border bg-amber-200 px-2 py-1 hovered:bg-amber-400">Action</button> | |
| <div class="rounded bg-teal-100 p-2 group-hovered-action:bg-teal-500 group-pressed-action:bg-teal-800">I change state when <span class="inline-block rounded p-1 font-bold">Group > Action</span> is hovered/pressed</div> | |
| <div class="rounded bg-red-500 p-2 text-white">I don't change</div> | |
| </div> | |
| <div class="rounded bg-green-100 p-2 peer-hovered-action:bg-green-500 peer-pressed-action:bg-green-800">I change state whn <span class="inline-block rounded p-1 font-bold">Peer > Action</span> is hovered/pressed</div> | |
| <div class="rounded bg-indigo-100 p-2 peer-hovered:bg-indigo-500 peer-pressed:bg-indigo-800">I change states when anything on <span class="inline-block rounded p-1 font-bold">Peer</span> is hovered/pressed</div> | |
| </div> | |
| <div class="grid gap-4 rounded-md border bg-white p-4 pressed-action:text-white"> | |
| <div class="group/card peer/card grid gap-2 rounded-md border p-4 hovered-action:bg-blue-100 pressed-action:bg-blue-500"> | |
| <h2 class="font-bold">group/card - peer/card - group-hovered-action/card</h2> | |
| <button type="button" class="action block rounded border bg-amber-200 px-2 py-1 hovered:bg-amber-400">Action</button> | |
| <div class="rounded bg-teal-100 p-2 group-hovered-action/card:bg-teal-500 group-pressed-action/card:bg-teal-800">I change state when <span class="inline-block rounded p-1 font-bold">Group > Action</span> is hovered/pressed</div> | |
| <div class="rounded bg-red-500 p-2 text-white">I don't change</div> | |
| </div> | |
| <div class="rounded bg-green-100 p-2 peer-hovered-action/card:bg-green-500 peer-pressed-action/card:bg-green-800">I change state whn <span class="inline-block rounded p-1 font-bold">Peer > Action</span> is hovered/pressed</div> | |
| <div class="rounded bg-indigo-100 p-2 peer-hovered/card:bg-indigo-500 peer-pressed/card:bg-indigo-800">I change states when anything on <span class="inline-block rounded p-1 font-bold">Peer</span> is hovered/pressed</div> | |
| </div> | |
| <div class="grid gap-4 rounded-md border bg-white p-4 pressed-[.selector]:text-white"> | |
| <div class="group peer grid gap-2 rounded-md border p-4 hovered-[.selector]:bg-blue-100 pressed-[.selector]:bg-blue-500"> | |
| <h2 class="font-bold">group - peer - group-hovered-[.selector]</h2> | |
| <button type="button" class="selector block rounded border bg-amber-200 px-2 py-1 hovered:bg-amber-400">Action</button> | |
| <div class="rounded bg-teal-100 p-2 group-hovered-[.selector]:bg-teal-500 group-pressed-[.selector]:bg-teal-800">I change state when <span class="inline-block rounded p-1 font-bold">Group > Action</span> is hovered/pressed</div> | |
| <div class="rounded bg-red-500 p-2 text-white">I don't change</div> | |
| </div> | |
| <div class="rounded bg-green-100 p-2 peer-hovered-[.selector]:bg-green-500 peer-pressed-[.selector]:bg-green-800">I change state whn <span class="inline-block rounded p-1 font-bold">Peer > Action</span> is hovered/pressed</div> | |
| <div class="rounded bg-indigo-100 p-2 peer-hovered:bg-indigo-500 peer-pressed:bg-indigo-800">I change states when anything on <span class="inline-block rounded p-1 font-bold">Peer</span> is hovered/pressed</div> | |
| </div> | |
| <div class="grid gap-4 rounded-md border bg-white p-4 pressed-[.selector]:text-white"> | |
| <div class="group/card peer/card grid gap-2 rounded-md border p-4 hovered-[.selector]:bg-blue-100 pressed-[.selector]:bg-blue-500"> | |
| <h2 class="font-bold">group/card - peer/card - group-hovered-[.selector]/card</h2> | |
| <button type="button" class="selector block rounded border bg-amber-200 px-2 py-1 hovered:bg-amber-400">Action</button> | |
| <div class="rounded bg-teal-100 p-2 group-hovered-[.selector]/card:bg-teal-500 group-pressed-[.selector]/card:bg-teal-800">I change state when <span class="inline-block rounded p-1 font-bold">Group > Action</span> is hovered/pressed</div> | |
| <div class="rounded bg-red-500 p-2 text-white">I don't change</div> | |
| </div> | |
| <div class="rounded bg-green-100 p-2 peer-hovered-[.selector]/card:bg-green-500 peer-pressed-[.selector]/card:bg-green-800">I change state whn <span class="inline-block rounded p-1 font-bold">Peer > Action</span> is hovered/pressed</div> | |
| <div class="rounded bg-indigo-100 p-2 peer-hovered/card:bg-indigo-500 peer-pressed/card:bg-indigo-800">I change states when anything on <span class="inline-block rounded p-1 font-bold">Peer</span> is hovered/pressed</div> | |
| </div> | |
| </div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import plugin from "tailwindcss/plugin"; | |
| type MaybeString = string | null | undefined; | |
| type UtilType = "group" | "peer"; | |
| // Main plugin | |
| export default plugin(function ({ matchVariant, theme, e }) { | |
| const values = { | |
| values: { | |
| DEFAULT: "", | |
| ...theme("betterStates", {}), | |
| }, | |
| }; | |
| const selector = (state: string, prefix: MaybeString) => { | |
| return `${prefix ?? "&"}:${state}:not(:disabled)`; | |
| }; | |
| const maybeWrap = (state: string, prefix = "&", value: MaybeString) => { | |
| return ["", null, undefined].includes(value) | |
| ? selector(state, prefix) | |
| : `${prefix}:has(${selector(state, value)})`; | |
| }; | |
| const merge = ( | |
| type: UtilType, | |
| value: MaybeString, | |
| modifier: string | null, | |
| callback: (value: MaybeString, prefix: string) => string[] | |
| ) => { | |
| const typeClass = modifier ? `.${type}\\/${e(modifier)}` : `.${type}`; | |
| const append = type === "group" ? " &" : " ~ &"; | |
| return callback(value, `:merge(${typeClass})`).map((x) => `${x}${append}`); | |
| }; | |
| // Hovered ________________________________________________________________________ | |
| const hovered = (value: MaybeString = null, prefix = "&") => [ | |
| maybeWrap("focus-visible", prefix, value), | |
| maybeWrap("hover", prefix, value), | |
| maybeWrap("has(:focus-visible)", prefix, value), | |
| ]; | |
| matchVariant("hovered", (value) => hovered(value), values); | |
| matchVariant( | |
| "group-hovered", | |
| (value, { modifier }) => merge("group", value, modifier, hovered), | |
| values | |
| ); | |
| matchVariant( | |
| "peer-hovered", | |
| (value, { modifier }) => merge("peer", value, modifier, hovered), | |
| values | |
| ); | |
| // Pressed ________________________________________________________________________ | |
| const pressed = (value: MaybeString = null, prefix = "&") => [ | |
| maybeWrap("active", prefix, value), | |
| maybeWrap("has(:active)", prefix, value), | |
| `@media (hover: none) { ${maybeWrap("hover", prefix, value)} }`, | |
| `@media (hover: none) { ${maybeWrap("focus", prefix, value)} }`, | |
| ]; | |
| matchVariant("pressed", (value) => pressed(value), values); | |
| matchVariant( | |
| "group-pressed", | |
| (value, { modifier }) => merge("group", value, modifier, pressed), | |
| values | |
| ); | |
| matchVariant( | |
| "peer-pressed", | |
| (value, { modifier }) => merge("peer", value, modifier, pressed), | |
| values | |
| ); | |
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const plugin = require('tailwindcss/plugin') | |
| /** @type {import('tailwindcss').Config} */ | |
| export default { | |
| theme: { | |
| extend: { | |
| betterStates: { | |
| action: '.action', | |
| }, | |
| }, | |
| }, | |
| plugins: [ | |
| plugin(function ({ matchVariant, theme, e }) { | |
| const values = { | |
| values: { | |
| DEFAULT: '', | |
| ...theme('betterStates', {}), | |
| }, | |
| } | |
| const selector = (state, prefix) => { | |
| return `${prefix ?? '&'}:${state}:not(:disabled)` | |
| } | |
| const maybeWrap = (state, prefix = '&', value) => { | |
| return ['', null, undefined].includes(value) | |
| ? selector(state, prefix) | |
| : `${prefix}:has(${selector(state, value)})` | |
| } | |
| const merge = (type, value, modifier, callback) => { | |
| const typeClass = modifier ? `.${type}\\/${e(modifier)}` : `.${type}` | |
| const append = type === 'group' ? ' &' : ' ~ &' | |
| return callback(value, `:merge(${typeClass})`).map((x) => `${x}${append}`) | |
| } | |
| // Hovered ________________________________________________________________________ | |
| const hovered = (value = null, prefix = '&') => [ | |
| maybeWrap('focus-visible', prefix, value), | |
| maybeWrap('hover', prefix, value), | |
| maybeWrap('has(:focus-visible)', prefix, value), | |
| ] | |
| matchVariant('hovered', (value) => hovered(value), values) | |
| matchVariant( | |
| 'group-hovered', | |
| (value, { modifier }) => merge('group', value, modifier, hovered), | |
| values, | |
| ) | |
| matchVariant( | |
| 'peer-hovered', | |
| (value, { modifier }) => merge('peer', value, modifier, hovered), | |
| values, | |
| ) | |
| // Pressed ________________________________________________________________________ | |
| const pressed = (value = null, prefix = '&') => [ | |
| maybeWrap('active', prefix, value), | |
| maybeWrap('has(:active)', prefix, value), | |
| `@media (hover: none) { ${maybeWrap('hover', prefix, value)} }`, | |
| `@media (hover: none) { ${maybeWrap('focus', prefix, value)} }`, | |
| ] | |
| matchVariant('pressed', (value) => pressed(value), values) | |
| matchVariant( | |
| 'group-pressed', | |
| (value, { modifier }) => merge('group', value, modifier, pressed), | |
| values, | |
| ) | |
| matchVariant( | |
| 'peer-pressed', | |
| (value, { modifier }) => merge('peer', value, modifier, pressed), | |
| values, | |
| ) | |
| }), | |
| ], | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** @type {import('tailwindcss').Config} */ | |
| export const theme = { | |
| extend: { | |
| betterStates: { | |
| action: ".action", | |
| }, | |
| }; | |
| export const plugins = [ | |
| require("./plugin-better-state"), | |
| ]; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment