Skip to content
meluizPublic

About

A collection of Tailwind CSS V4-ready utilities.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

49 Commits

Folders and files

Repository files navigation

tailugin

A collection of Tailwind CSS v4 utilities: enter/exit animations, semantic state variants, OKLCH color palettes, and easing tokens — all authored with native v4 directives (@utility, @custom-variant, @theme), so they compose exactly like built-in classes.

npm version npm downloads license

Requires Tailwind CSS v4. tailugin uses the v4 CSS-first architecture. It does not work with v3.

Installation

bun add tailugin
# or
npm install tailugin

Usage

Import Tailwind, then tailugin, in your entry CSS:

@import "tailwindcss";
@import "tailugin";

That's the whole setup. Everything below is now available as utility classes and variants.

Prefer to pull in only what you need? Every module has its own entry point — see Granular imports.

Modules

tailugin is organized into four independent modules:

Module What it adds Import
theme 13 color palettes + 22 easing tokens tailugin/theme
variants 40 semantic state variants tailugin/variants
utilities Animation, sizing and alignment tailugin/utilities
preflight Typographic refinements tailugin/preflight

Colors

13 OKLCH palettes, each with a 12-step ramp (50–975). They add to Tailwind's default colors — nothing native is overwritten, so bg-blue-500 and bg-cactus-500 coexist.

Color palettes

<div class="bg-cactus-500 text-clay-50 border-dusk-200">…</div>

Available palettes: cactus, clay, dusk, fig, heather, kraft, lagoon, manila, moss, orchid, silt, sumac, woad.

Every palette is built the same way — one lightness ramp, one chroma curve, and a single hue — so swapping one for another keeps contrast consistent across your UI.

sumac (red) and woad (indigo) fill the two widest gaps on the hue wheel. They are named after the dye plants rather than the colors so they never shadow Tailwind's own red and indigo, which stay available alongside them.

Picking a step

Because the lightness ramp is shared, every palette crosses the WCAG thresholds at the same step. Pick the step by its job, not by the color:

On a light surface Passes Contrast Use for
50–200 — 1.1–1.5 Page and card backgrounds, subtle fills
300–400 — 1.9–2.8 Dividers, disabled text, decorative only
500 ≥ 3:1 3.5–3.9 Borders, icons, focus rings, large headings
600 ≥ 4.5:1 5.0–5.5 Body text — the lowest step that passes AA
700–975 ≥ 7:1 7.0–19.5 Body text and headings, AAA
On silt-950 (dark) Passes Contrast Use for
50–400 ≥ 4.5:1 6.3–15.9 Body text and headings
500 ~4.5:1 4.48–4.97 Large text and headings (see note)
600 ≥ 3:1 3.2–3.5 Borders, icons, focus rings
700–900 — 1.4–2.5 Raised surfaces, dividers, decorative only
975 — 1.3 The surface below the page background

On a dark surface, step 500 sits right on the AA line. It clears 4.5:1 for eleven palettes but lands at 4.48 and 4.49 for sumac and clay, the two most saturated ones. Use 400 when the text is small.

Every swatch fits inside sRGB, so nothing shifts on displays without a P3 gamut.

The dark end

950 is a charcoal rather than a near-black, which is deliberate — the ramp is drawn for light interfaces first. 975 is the extra anchor below it, six lightness points down, the same gap that separates 800 from 900. That is enough separation to read as elevation without a shadow:

Layer Step
Page background 975
Raised surface 950
Border 900 / 800

Animations

Enter/exit animations built on two base utilities — animate-in and animate-out — combined with modifier utilities that describe the motion.

<!-- A popover that fades, zooms, and slides in from the top -->
<div class="animate-in fade-in zoom-in slide-in-top-2 duration-200">…</div>

How it works

animate-in / animate-out are the triggers — they start the animation. The modifiers (fade-in, zoom-in, slide-in-top-*, …) only describe how the element moves; on their own they do nothing until a trigger runs. This means you can put the modifiers on the element unconditionally and only toggle the trigger:

<div
  class="fade-in slide-in-top-2 fade-out slide-out-top-2
         is-open:animate-in is-closed:animate-out duration-200"
>
  …
</div>

The state prefix appears twice (on the triggers), not on every modifier.

Timing

Duration and easing use Tailwind's native utilities — no special classes needed:

<div class="animate-in fade-in duration-300 ease-out-expo">…</div>
  • duration-* — sets the animation duration (defaults to 150ms).
  • ease-* — sets the timing function (see Easings).
  • delay-* — sets the animation delay.

Modifiers

Each modifier works without a value (using a sensible default) or with one for full control.

Utility Without value With value
fade-in / fade-out opacity from 0 fade-in-90 (from 90%), fade-in-[0.7]
zoom-in / zoom-out scale from 0.95 zoom-in-50, zoom-in-[0.8]
blur-in / blur-out blur 4px blur-in-8, blur-in-[2px]
spin-in / spin-out requires a value spin-in-90, -spin-in-45, spin-in-[30deg]
slide-in-{top,bottom,left,right} requires a value slide-in-top-8, slide-in-left-[3rem]
slide-out-{top,bottom,left,right} requires a value slide-out-top-8

Naming: in means where it comes from, out means where it goes to. slide-in-top enters from above; slide-out-top exits upward.

Controls

Utility Sets
fill-mode-{none,forwards,backwards,both} animation-fill-mode
direction-{normal,reverse,alternate,alternate-reverse} animation-direction
repeat-{n,infinite} animation-iteration-count
play-state-{running,paused} animation-play-state

Scroll-driven

The same modifiers can be driven by scroll position instead of by time. Add a timeline and the animation plays as the element moves through the scrollport:

<div class="animate-in fade-in slide-in-bottom-4 timeline-view timeline-entry">…</div>
Utility Sets
timeline-view Driven by the element crossing the scrollport
timeline-scroll Driven by the nearest scroll container
timeline-scroll-root Driven by the document's scroll progress
timeline-scroll-self Driven by the element's own scroll progress
timeline-none Detaches, back to a time-based animation
timeline-{cover,contain,entry,exit,entry-crossing,exit-crossing} animation-range

A progress-based timeline needs animation-duration: auto, which the timeline-* utilities supply for you. An explicit duration-* still wins, so you can mix the two freely.

For a compound range, reach for the arbitrary property:

<div class="animate-in fade-in timeline-view [animation-range:entry_0%_cover_50%]">…</div>

Scroll-driven animation is not supported everywhere. Where it is missing, animation-timeline is ignored and the animation simply plays once on load — the same entrance you would get without a timeline.

Reduced motion

animate-in and animate-out respect prefers-reduced-motion: reduce automatically — animation is disabled for users who ask for less motion, scroll-driven included.


Sizing

Transitioning to auto

CSS cannot interpolate between a length and a keyword, so height: 0 to height: auto snaps instead of animating. interpolate-allow-keywords opts a subtree into resolving the keyword and animating towards it — no wrapper element, no measuring in JavaScript, no max-height guess:

<html class="interpolate-allow-keywords">
  <div class="h-0 is-expanded:h-auto transition-[height] duration-200">…</div>
</html>

It covers every intrinsic keyword, not only auto — min-content, max-content and fit-content interpolate too, so a dropdown can animate to the width of its contents.

Utility Sets
interpolate-allow-keywords Keywords interpolate
interpolate-numeric-only The CSS default — keywords snap

The property is inherited, which is why both values ship: set allow-keywords once on the root, then use numeric-only to opt a subtree back out where a snap is what you want.

Support is not universal. Where the property is missing the declaration is ignored and sizes snap — exactly the behavior you have without it.


Alignment

align-items and justify-content are written together far more often than apart, and CSS has no shorthand for the pair — place-items sets justify-items, which does nothing in a flex container. These utilities collapse the two into one class, named {align}-{justify}:

<div class="flex center">…</div>
<div class="flex center-between">…</div>
<div class="flex baseline-evenly">…</div>
Axis Values
align center, start, end, stretch, baseline
justify center, start, end, between, around, evenly

Where both axes take the same keyword there is only the short form: center, start and end set both at once. They are the only three keywords valid on both axes, so there is no center-center to write.

These set alignment only. Add flex or inline-flex yourself, so the same classes stay usable on a container you have already made flex elsewhere.


Variants

Semantic state variants that match native, data-*, and ARIA attributes at once — so they work across headless UI libraries (Radix, Ark, React Aria, Base UI) without configuration.

<div data-state="open" class="is-open:opacity-100 is-closed:opacity-0">…</div>
<li aria-selected="true" class="is-selected:bg-fig-100">…</li>

Variants that describe an element's state are prefixed with is-, so they never collide with — and instead extend — Tailwind's native open:, checked:, disabled:, starting: and friends. Variants that describe where or how something sits (placement, orientation, navigation role, calendar position) use plain names.

Group Variants
Interaction is-disabled, is-dragging, is-drop-target
Disclosure is-open, is-closed, is-expanded, is-collapsed
Transition is-starting, is-ending
Selection is-highlighted, is-selected, is-checked, is-unchecked, is-indeterminate, is-pressed, is-on, is-off
Form is-invalid, is-valid, is-required, is-readonly
Calendar is-today, is-unavailable, range-start, range-end, outside-month
Placement side-top, side-right, side-bottom, side-left, side-inline-start, side-inline-end, align-start, align-center, align-end
Navigation current-page, current-step
Orientation horizontal, vertical
Theme dark

Use the native open: / disabled: / checked: for plain HTML. The is- versions add the data-* and ARIA state that headless components emit on non-native elements.

Open vs. expanded

These are two different states, kept apart on purpose:

  • is-open / is-closed — an overlay or panel that mounts and unmounts: dialog, popover, menu, tooltip, accordion panel.
  • is-expanded / is-collapsed — a trigger, row, or tree node that reports aria-expanded: disclosure button, combobox input, expandable table row.
<!-- the trigger rotates, the panel animates -->
<button aria-expanded="true" class="is-expanded:[&_svg]:rotate-180">…</button>
<div data-state="open" class="is-open:animate-in is-closed:animate-out">…</div>

Transition states

is-starting / is-ending match the mount and unmount phases that Base UI (data-starting-style / data-ending-style) and React Aria (data-entering / data-exiting) expose — so they pair directly with the animation utilities:

<div class="is-starting:animate-in is-starting:fade-in is-ending:animate-out is-ending:fade-out duration-200">…</div>

Placement

Floating elements are positioned on two axes, and every library exposes them as two separate values — a side and an alignment. There is no single top-left state; you compose the two variants instead:

<!-- anchored above the trigger, aligned to its start edge -->
<div class="side-top:align-start:origin-bottom-left">…</div>

<!-- enter animation follows the side the popover landed on -->
<div class="animate-in fade-in side-top:slide-in-bottom-2 side-bottom:slide-in-top-2">…</div>
Axis Variants Source
Side side-top, side-right, side-bottom, side-left data-side (Radix, Ark, Base UI), data-placement (Ark, React Aria)
Side side-inline-start, side-inline-end data-side (Base UI only — logical, resolves against direction)
Alignment align-start, align-center, align-end data-align (Radix, Base UI, Ark)

Alignment coverage is uneven upstream: Ark's popper components encode it inside data-placement (top-start), which align-start / align-end also match — but Ark never reports a centered alignment, and React Aria's data-placement carries no alignment at all.

Notes on coverage

  • is-readonly deliberately omits the native :read-only pseudo-class, which matches every non-editable element (including plain divs). Use read-only: for native inputs.
  • is-invalid / is-valid use :user-invalid / :user-valid rather than :invalid / :valid, so they only fire after the user has interacted with the field.
  • is-checked includes [data-selected] because React Aria reports checkboxes, radios, and switches as selected rather than checked. On React Aria this makes is-checked: and is-selected: equivalent.
  • is-on / is-off are toggle-button states (data-state="on"). A switch reports itself as checked, so use is-checked: there.

Easings

22 easing tokens exposed via @theme, so they become ease-* utilities automatically and work with any transition or animation:

<button class="transition ease-out-expo">…</button>
<div class="animate-in fade-in ease-anticipate">…</div>

Available: anticipate, quick-out, in-out, in-out-base, and the in/out/in-out sets for quad, cubic, quart, quint, expo, and circ (e.g. ease-in-quad, ease-out-expo, ease-in-out-circ).

Note: tailugin redefines the native ease-in-out token with a refined curve. If you rely on Tailwind's default ease-in-out, be aware it is overridden.


Preflight

Optional typographic refinements layered on top of Tailwind's own Preflight (which still handles the reset). Adds automatic hyphenation, balanced headings, pretty paragraph wrapping, tabular numerals, and code ligatures.

@import "tailwindcss";
@import "tailugin/preflight";

It only adds what Preflight doesn't cover — no reset is duplicated. Unsupported properties are ignored by the browser without breaking layout.


Granular imports

Import only the modules — or sub-modules — you need:

@import "tailugin/theme"; /* colors + easings */
@import "tailugin/variants"; /* all variants */
@import "tailugin/utilities"; /* alignment + animation + sizing */
@import "tailugin/preflight";

/* Sub-modules */
@import "tailugin/theme/colors";
@import "tailugin/theme/easings";
@import "tailugin/utilities/alignment";
@import "tailugin/utilities/animate";
@import "tailugin/utilities/size";

/* A single variant group — calendar, disclosure, form, interaction, navigation,
   orientation, placement, selection, theme, transition */
@import "tailugin/variants/placement";

/* A single color palette */
@import "tailugin/colors/fig";

License

MIT © meluiz

About

A collection of Tailwind CSS V4-ready utilities.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Contributors

Languages