Skip to content

Instantly share code, notes, and snippets.

@Neophen
Last active December 20, 2024 07:46
Show Gist options
  • Save Neophen/2f512ace1e7182e5346076333e4a0fdc to your computer and use it in GitHub Desktop.
Save Neophen/2f512ace1e7182e5346076333e4a0fdc to your computer and use it in GitHub Desktop.
Better states, plugin for tailwind

Better hover/active states, Tailwind CSS

Preview:

example_states.mov

Tailwind Play link:

https://play.tailwindcss.com/Aj7D2OHw20

Use case: A card with main action and secondary action

example_card_with_actions.mov

Tailwind Play link:

https://play.tailwindcss.com/6oReKHMiQG

HTML if the link is broken

<div class="grid gap-6 bg-slate-100 p-4">
  <h1>Use case: card with main link and other actions</h1>
  <div class="rounded-md border bg-white p-4">
    <div class="group relative flex items-center justify-between rounded-md border p-4 hovered-action:bg-blue-100 hovered-action:ring-2 hovered-action:ring-black pressed-action:bg-blue-600">
      <p class="font-bold group-hovered-action:text-blue-600 group-pressed-action:text-white">Mykolas Mankevicius</p>
      <div class="flex gap-4">
        <button type="button" class="block rounded border bg-white px-2 py-1 hovered:bg-red-50 hovered:text-red-600 z-10">Remove</button>
        <button type="button" class="action block rounded border bg-slate-600 px-2 py-1 text-white before:absolute before:inset-0 hovered:bg-slate-900">View Profile</button>
      </div>
    </div>
  </div>
</div>
<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 &gt; 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 &gt; 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 &gt; 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 &gt; 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 &gt; 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 &gt; 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 &gt; 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 &gt; 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>
import plugin from 'tailwindcss/plugin'
type MaybeString = string | null | undefined
type UtilType = 'group' | 'peer'
// Example
// <div class="group relative hovered-action:bg-blue-100 pressed-action:bg-blue-600">
// <p class="group-hovered-action:text-blue-600 group-pressed-action:text-white">Mykolas Mankevicius</p>
// <button class="hovered:bg-red-50 hovered:text-red-600 z-10">Remove</button>
// <button data-action class="before:absolute before:inset-0 hovered:bg-slate-100">View Profile</button>
// </div>
// Main plugin
export default plugin((api) => {
const betterStates = api.theme('betterStates', {})
const values = {
values: {
DEFAULT: '',
...betterStates
}
}
const selector = (state: string, prefix: MaybeString) => {
return `${prefix ?? '&'}:${state}:not(:disabled)`
}
const maybeWrap = (state: string, prefix: string, 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, merge: boolean) => string[]
) => {
const typeClass = modifier ? `.${type}\\/${api.e(modifier)}` : `.${type}`
const append = type === 'group' ? ' &' : ' ~ &'
return callback(value, `:merge(${typeClass})`, true).map((x) => `${x}${append}`)
}
// Hovered ________________________________________________________________________
const hovered = (value: MaybeString = null, prefix = '&', merge = false) => {
const postfix = merge ? ' & ' : ''
return [
`@media (hover: hover) and (pointer: fine) { ${maybeWrap('hover', prefix, value)}${postfix}}`,
`@media (hover: hover) and (pointer: fine) { ${maybeWrap('focus-visible', prefix, value)}${postfix}}`,
`@media (hover: hover) and (pointer: fine) { ${maybeWrap('has(:focus-visible)', prefix, value)}${postfix}}`
]
}
api.matchVariant('hovered', (value) => hovered(value), values)
api.matchVariant('group-hovered', (value, { modifier }) => merge('group', value, modifier, hovered), values)
api.matchVariant('peer-hovered', (value, { modifier }) => merge('peer', value, modifier, hovered), values)
// Pressed ________________________________________________________________________
const pressed = (value: MaybeString = null, prefix = '&', merge = false) => {
const postfix = merge ? ' & ' : ''
return [
maybeWrap('active', prefix, value),
maybeWrap('has(:active)', prefix, value),
// Wrap the entire selector in the media query, not just the state
`@media (hover: none) or (pointer: coarse) { ${maybeWrap('hover', prefix, value)}${postfix}}`,
`@media (hover: none) or (pointer: coarse) { ${maybeWrap('focus-visible', prefix, value)}${postfix}}`,
`@media (hover: none) or (pointer: coarse) { ${maybeWrap('has(:focus-visible)', prefix, value)}${postfix}}`
]
}
api.matchVariant('pressed', (value) => pressed(value), values)
api.matchVariant('group-pressed', (value, { modifier }) => merge('group', value, modifier, pressed), values)
api.matchVariant('peer-pressed', (value, { modifier }) => merge('peer', value, modifier, pressed), values)
})
/** @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