package tw
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
On This Page
Type-safe Tailwind CSS v4 in OCaml
Install
dune-project
Dependency
Authors
Maintainers
Sources
tw-1.1.0.tbz
sha256=48754ab34d0a97c37f5f2dbf50ce46747ec0ca6d483f5adbb7305fc247fb5315
sha512=f43621b49e77adc23fab3c968e5041188e428228d1930b89c307fc8916c428f1943a5d74c21467219077247021f0ba83fda9234b0dd119dfd14b7f9746332bf7
doc/CHANGES.html
1.1.0
Breaking changes
- Variable ordering and
@propertymetadata travel withVarvalues andStyle.trather than through process-global registries.Var.order,Var.family,Var.property_order,Var.needs_property,Var.register_property_orderandVar.resolve_theme_refsare removed, and the publicStyle.Stylerecord gains ametadatafield. Construction, parsing and rendering are safe to run from several OCaml domains without a mutex, and the typedproperty_defaultrules are generated (#653). bg_transparent,bg_currentand the background colour constructors live inBackgrounds, andborder_color,border_transparentandborder_currentinColor; the copiesColorandBorderscarried are gone. Building one from OCaml and parsing the same class name reached different handlers with different sort slots (#518).- Every spacing utility takes an
int.space_x,space_y,indent,border_spacingand thescroll_m*andscroll_p*families took afloat; a primed variant takes one now, sospace_x 2.5becomesspace_x' 2.5(#492). - An unrecognised class no longer raises.
Tw.strraised,Tw_htmlcopied the name in silently, andTw_dom.use_strcrashed the browser render. Unknown names pass through everywhere, never raise from a rendering path, and are reportable instead:Tw.of_classesreturns them alongside the utilities, andTw_html.unknown_classesandTw_dom.unknown_classesexpose the same list. Code that caught the exception to find a typo reads that list.Tw.of_stringis unchanged, so the CLI still reports a deliberately typed class as an error (#514). -safealignment resolves oncenterandendonly, as in Tailwind. The-start-safeclasses stop being emitted, andAlignment.content_start_safeandAlignment.place_items_start_safeare removed (#679).- A project build emits the typography plugin's
proseutilities only when its entrypoint declares@plugin "@tailwindcss/typography", as Tailwind does.tw src/with no entrypoint wrote the plugin's whole stylesheet for anyprosein the markup;tw -s prosestill compiles the class on its own (#142). reis a regular dependency rather than a test-only one. The CLI and the dev tools use it to search text no one typed; it stays out of thetw.dombundle, which links neither (#774).
Tailwind CSS 4.3.3
- Track Tailwind CSS 4.3.3, from 4.3.1.
font-sanscarries the 4.3.2 system stack, preflight scopes:-moz-focusringto non-iframe elements, and an achromatic colour writes its powerless hue asnone(#128, #129, #130, #132, #147). - The mauve, mist, olive and taupe palettes, declared between
stoneandblackand sorting among the rest where Tailwind puts them (#153, #696).
Project stylesheets
twcompiles a project's CSS entrypoint.--input-cssfed the file to Tailwind for--diffand read a few@themeoverrides off it; now the whole stylesheet is the input, as it is fortailwindcss: the directives below expand, the author's own rules go through in their own spelling with the browser-compatibility prefixes the CLI writes, and Tailwind's at-rules (@theme,@source,@plugin,@config,@reference,@tailwind) stay out of the sheet a browser reads. The@supports-guarded initial values of the utility variables are written once, whatever the entrypoint pulls in (#136, #138, #139, #140, #141, #143, #195, #206, #361, #665, #687).@import "tailwindcss"takes its options.theme(static)declares the whole theme,@keyframesincluded, once;source(none)andsource("../src")decide what is scanned;prefix(tw)putstw:in front of every candidate, on the rule a stacked variant nests under its query and on thegroup/peeranchors, and--tw-in front of the theme's own tokens, the--tw-*channels a utility sets and avar(--brand)the author wrote keeping their names;importantmarks every utility declaration!important, variants and declared@utilityrules included, while author CSS stays as written; andlayer(…)puts an imported file's rules in that layer with its theme tokens,@propertyregistrations and keyframes at the top of the sheet.tailwindcss/theme.css,tailwindcss/utilities.cssand@tailwind utilitieseach bring their own part of the sheet, and@reference "tailwindcss"puts the theme in scope for@applyand emits none of it, each token an applied utility reads carrying its value as thevar()fallback. A file the entrypoint imports from beside it is read under--diffand--tailwindtoo (#781, #788, #793, #794, #796, #801, #816, #844, #849).- A project's
@themereaches every utility generated from the markup: its colours, spacing, shadows, blur radii, timing functions, font weights, line heights, letter spacing, corner radii, font sizes, perspectives, aspect ratios, max-widths, fonts, breakpoints, container sizes,@keyframesand animations, and a token a utility reads is declared in the theme layer. A size carries the--text-<name>--line-height,--letter-spacingand--font-weightdeclared beside it, a family its--font-feature-settingsand--font-variation-settings, a container size of the project's own names a query (@hero:,@min-hero:,@hero/main:) sorted among the built-in sizes by width, a breakpoint of the project's own sorts with the scale, and a project shadow takes an opacity (#170, #179, #193, #221, #227, #229, #230, #261, #315, #320, #447, #450, #457, #458, #459, #460, #461, #462, #463, #464, #510, #725, #847).--name: initialremoves a token and--namespace-*: initiala whole namespace, in every family: a removed breakpoint or container size names no variant, and a removed colour, font, size, radius, shadow, animation or any other token no utility reading it, as Tailwind compiles nothing for the candidate; a size query reads the width the block binds to--container-<size>(#507, #515, #847). The bare--*: initialreaches the--default-*tokens the way Tailwind reads them, as--theme(--default-<x>, <fallback>): preflight writes its font stacks andnormalthemselves and the transition familyeaseand0s(#847).@theme staticdeclares its tokens whether or not a utility reads them;@theme inlinefolds a token's value into every utility that reads it,p-4writingcalc(.25rem * 4)under an inline--spacingandtext-brandwritingvar(--brand)under the shadcn-style--color-brand: var(--brand), in every colour family and opacity form; and@theme referencedeclares nothing, a utility reading its token carrying the value as thevar()fallback (#783, #790, #795, #826, #847). A project's own@propertygets the@supports-guarded initial value in@layer propertiesthat Tailwind writes for its own (#791). @utilitydeclares a utility, static or functional.@utility tab-*resolves--value()and--modifier()with their data-type and theme arguments, every declaration made for one name applies, a body may@applyanother utility or nest@variant, and the utility sorts after the built-ins of its family, as Tailwind puts it, so on an element carrying both the declared one wins. A candidate a functional declaration declines falls to the built-in utility of its root, as Tailwind tries every one registered, a!on a declared utility marks its declarations!important, a malformed declaration costs only its own class, andtw -s CLASS --input-css ENTRYgenerates a declared utility as the scanning form does (#516, #526, #550, #554, #650, #651, #655, #785, #798, #847).@applypulls a utility's declarations into the author's rule, variants included, a run of plain utilities landing in one rule as Tailwind writes them. The theme tokens,@propertyregistrations and@keyframesthe applied utilities read go at the top of the sheet, and@apply prosekeeps the.prosethe typography plugin scopes its nested-list spacing to (#779, #799).@custom-variantdeclares a variant, with a selector or at-rule body in the@slotor the shorthand form, and@variant NAME { … }uses it, or a built-in one, in author CSS. A declareddarkwins over the built-in and keeps its sort slot, one built on a named@containerquery keeps the name,not-negates the project's declaration (a body of two branches is refused, as Tailwind refuses it), a class the variant routes is generated once by the utility that owns it, and the variant sits where the candidate wrote it among the built-in ones, as Tailwind applies them left to right:hover:dark:text-whiteis.x:hover:where(.dark, .dark *)anddark:hover:text-white.x:where(.dark, .dark *):hover(#564, #672, #673, #836, #839, #847).@source "<path>"scans a path relative to the stylesheet: a directory is walked, a glob matches under its root, and@source nottakes files back out.@source inline("…")safelists its classes with Tailwind's brace patterns ({hover:,}bg-red-{500,600},p-{0..8..4}) and@source not inline("…")blocks them, markup included (#787, #806).@plugin "@tailwindcss/forms"writes the plugin's base reset of native form controls, in@layer baseeven when the entrypoint imports Tailwind without preflight, unless its options ask forstrategy: "class"(#804, #817).theme(),--theme(),--spacing()and--alpha()expand in author CSS.theme(--color-red-500)and the v3theme(colors.red.500)give the token's value;--theme(--x)avar()the theme layer declares, with its fallback, or the value itself underinlineand in an at-rule prelude;--spacing(4)the spacing arithmetic; and--alpha(red / 50%)thecolor-mix()it spells, with the legacy fallback and@supportsarm beside it (#780, #783, #792).- An entrypoint carrying a v3
@configis refused:twexits with an error naming the file and pointing at@theme(#800).
Command line
tw --diffandtw --tailwindfind atailwindcssornpxonPATHwithout askingwhich, which Arch Linux does not install; the lookup, like the project's ownnode_modules/.bin/tailwindcss, skips a file that is not executable.-qsays what it suppresses, and the scan statistics header no longer ends in a stray@.(#887).- An installed
tw --diffortw --tailwindno longer leaves atmp/directory in the project; its scratch files go undernode_modules/.cache/twand are removed on exit (#886). --htmlis read against--cwd, like-i,-oand the paths to scan (#885).twtakes the Tailwind CLI's flags, sotw -i src/app.css -o dist/app.css --watchruns wheretailwindcssdid:-i,-o,-m,-w,--poll,--cwdand--silent. With no path given, sources are detected from the working directory as the import'ssource()says, and a.csspath to scan is refused rather than read as markup (#837).- Class scanning covers Markdown, MDX, JavaScript and TypeScript, and finds where a candidate ends with cascade's tokeniser rather than by guessing: a bracket before whitespace is not a candidate, a candidate stops at the end of its line, a
/modifieropening on-or_is none, and a recursive scan stays inside the source tree (#137, #145, #208, #288, #318, #321, #564, #772). - Compiling a project is fast: tailwindcss.com's class list takes 0.3 s of CPU, where
tailwindcsstakes 0.6 s (#843, #845, #846). tw --diff --html PAGEalso renders both stylesheets overPAGEin a headless Chromium, compares the pixels, and says when the render and the structural diff disagree: an entry listed over a page that paints alike is named an over-report of the differ, and a render that differs where nothing was listed an under-report, each a cascade bug to file (#841, #873).tw --diffexits 1 when the two sheets differ and 2 when it cannot read one of them, so a CI job can gate on it. It printed the differences and exited 0 (#810).tw --tailwindandtw --difffind the pinned CLI from a directory with nonode_modulesabove it, no longer compile the working directory before they start, and say when a CLI of the right version cannot compile from the working directory. The reference they build no longer reports a difference for a class Tailwind's own extractor declines to read back, or for atheme(--x)read the CLI binds only for a candidate found in a file (#702, #705, #712, #727, #821).tw --inlineresolves the spacing token and the arithmetic it leaves:p-4printspadding: 1remand no theme block. It kept--spacingdeclared and wrotecalc(var(--spacing) * 4)(#820).- Conflicting backend flags are refused rather than one being picked silently (#317).
Utilities
- A palette name in a bracket is the identifier it is, as in Tailwind:
bg-[emerald],bg-[slate]andtext-[rose]wrote the palette's 500 shade, a colour the page does not have. Tailwind writesbackground-color: emeraldthrough and a browser drops it. A CSS colour in a bracket, a keyword or a system colour included, still resolves (#879). - A palette name with no shade names no utility, as in Tailwind:
bg-red,text-red,from-red,ring-redand their kin compiled a.bg-red-500rule nobody wrote.black,white,current,transparent,inheritand a project's own--color-redtoken still resolve (#874). - A fraction resolves exactly, as Tailwind's
calc(1/3 * 100%)does:w-1/3rendered 106.984px against 107px in a 321px container. The sizing, inset, flex and basis families share it (#828). - Sizing accepts the whole scale. The container scale reaches the logical families and
basis-*, both viewport axes and thepxstep work on the width and height families, and a fraction takes any denominator, including zero and improper ones.w-3/8,min-w-7/12,max-h-dvw,size-px,basis-7xland@container-sizeall resolve (#146, #151, #152, #154, #155, #156, #159, #180, #207, #216, #564). - Position and translate read a spacing step in either sign and a fraction of any shape, and a negated arbitrary inset accepts a parenthesised calc body, so
-left-6/5,-top-2.5,-left-[(var(--a)+var(--b))]andtranslate-2work alongside the numeric steps, and-translate-y-0.5sorts in the negative band of its axis with the integers. A negated bracket translate is negated as Tailwind writes it on every axis,-translate-z-[4px]givingcalc(4px * -1)and-translate-x-[calc(1rem+2px)]the calc negated, where the z axis read the bracket as a variable name and any bracket that was not a plain length became avar()of its own text. A negated bracket scale, skew and rotate go the same way on every axis,-scale-[1.5]givingcalc(1.5 * -1)and-skew-x-[10deg]skewX(calc(10deg * -1)); the first two were unknown classes, and-rotate-[.5turn]wrote its angle un-negated (#160, #166, #172, #186, #210, #646, #831, #861, #865). - Every inset side carries the whole scale, under either sign,
start-*,end-*and the logicalinset-s-*,inset-e-*,inset-bs-*andinset-be-*included: a spacing step,px, a fraction, an arbitrary length, and a name the theme binds through--inset-<name>or--spacing-<name>.start-px,start-1/2,-top-[4px],-inset-bs-[4px],inset-s-0.5,inset-bs-1/2and-bottom-3/4all reach the sheet (#677, #691, #708). - Transforms, backgrounds, grids and typography take the keywords Tailwind documents:
translate-none,rotate-none,scale-none,perspective-near,duration-initial,ease-initial,via-none,grow-3,indent-pxand a negative-indent-4. A zero translate keeps its unit,aspect-[1.333]takes a bare number,grid-cols-[min(50%,20rem)]takes a math function,bg-position-[center_2rem]keeps both axes, andcontent-[attr(before)]takes an unquoted function (#134, #157, #164, #171, #174, #175, #178, #181, #183, #184, #218, #223). - Borders and masks cover their logical and arbitrary forms: axis and single-side widths and colours (
border-x-16,border-bs-red-500), any integer width or outline offset, a mask colour stop, a bracket mask image, a zero mask stop that keeps its unit, and a bracket mask stop written as Tailwind decodes it,mask-linear-from-[calc(1px+2px)]givingcalc(1px + 2px)where the undecoded text was a value browsers drop. A mask stop reads as Tailwind classifies it: a bracket colour, an--alpha()or acolor:hint name the stop's colour and a length, a--xor a--spacing(4)its position, a bare step counts in quarters, a percentage is a whole number, and a colour stop takes an opacity modifier,mask-linear-from-red-500/50mixing as every colour family does (#148, #161, #162, #163, #165, #182, #222, #265, #860, #866). transition-behavior-normalandtransition-behavior-allow-discreteare refused, as Tailwind compiles nothing for them. They compiled to a rule fortransition-normalandtransition-discrete, which no markup written the long way matches (#797).- A v3 opacity utility says so.
bg-opacity-50and thetext-,border-,divide-,ring-andplaceholder-spellings are still refused, as Tailwind refuses them, but the message names the v4 replacement (bg-<color>/50) instead of reporting an unfamiliar name, and the CLI'sWarning:line carries what the parser said rather than alwaysUnknown class(#777).
Arbitrary values
- The
(--name)shorthand reaches every place a bracket does:text-sm/(--lh)sets the line height beside the size, and a colour's(--c)takes an opacity modifier,bg-(--c)/50,text-(--c)/50andbg-(--c)/(--o)compiling to Tailwind'scolor-mix()under@supports. All were unknown classes (#811, #812). - A data-type hint comes off the front of any bracket, whatever the family does with what follows, and the value after it is what reaches the sheet, the hint kept in the class name.
z-[integer:5]crashed the run,text-[length:1.25rem]wrotefont-size: var(--1\.25rem)andcol-[foo:2]wrotegrid-column: foo; all now write the value the author meant, as dodivide-[color:red],shadow-[length:3px],rotate-[angle:45deg],aspect-[ratio:16/9],border-[line-width:2px],bg-[percentage:50%],transform-[foo:scaleX(2)]and every other family. A hint's name is a run ofa-zand-, somask-[FOO:2em]holds its bracket whole, and a bracket whose hint is empty or which holds nothing but blank space names no utility, as in Tailwind (#706, #714, #718, #720, #729, #730, #731, #732, #733). - A bracket no reader takes goes to the longhand the class names, as Tailwind writes it, rather than being refused, which dropped the rule and left an element carrying the class with nothing at all.
text-[length:red],text-[notacolour],stroke-[1zz],bg-[image:nope],p-[foo],top-[foo],w-[foo],rounded-[foo],mask-[foo]and a malformed hex in any colour family reach the sheet as declarations the browser discards; a bare number after a width hint is pixels, the spelling Tailwind's minifier ships; andbg-size-[foo]no longer writesbackground-size: auto, a value the class never asked for. Every one of the 150 families the token-stream contract inventories does this, against 38 before (#761 through #771). - An arbitrary value reads through the same CSS grammar as the rest of the sheet, so a length, angle, colour, shadow, ease, blur, tracking, line-height, stroke width, border-spacing or gradient stop takes every unit and math function CSS allows, and a compact
calc(), avar()with its fallback, atheme()in dot notation, a--spacing()call, an--alpha()call, a grid track and a list style mean the same thing wherever they appear:ml-[50%],left-[calc(5%-2px)],py-[calc(--spacing(2)+1px)],list-[upper-roman],gap-[calc(1px_+_1px)],mx-[--spacing(4)],flex-[calc(1+2)],z-[calc(1+2)],delay-[calc(1s+2s)],text-[--alpha(red/0.2)]andorigin-[--spacing(4)_--spacing(2)]all resolve, a bracket colour is read as CSS before the palette is consulted, and a value the target property cannot take is still written through as Tailwind writes it. An--alpha()expands anywhere in a value, as--spacing()does, its alpha a number, a percentage or avar():shadow-[0_0_0_1px_--alpha(red/50%)],bg-[linear-gradient(--alpha(red/0.5),blue)]andtext-[--alpha(red/var(--o))]resolve, a bare number scales as Tailwind scales it,0.2being 20%, and a call missing either half names no utility. A named opacity on a shadow's size,shadow-lg/half, sets no alpha, since Tailwind reads none there (#168, #176, #177, #187, #188, #189, #190, #191, #192, #205, #212, #217, #236, #241, #262, #277, #278, #325, #371, #372, #373, #375, #376, #377, #378, #404, #417, #418, #420, #465, #503, #504, #509, #522, #667, #683, #688, #689, #690, #863, #869). - A bracket only OCaml's number reader accepts is no longer folded to a different value.
tab-[0x4]wrotetab-size: 4,flex-[0x4]wroteflex: 4under the class name.flex-\[4\],grid-cols-[0x4]wrote4px,aspect-[0x4]was refused, andstroke-0x4emitted a.stroke-4nobody wrote,/0x50rode the opacity modifier onto every colour utility andmin-[0x600px]manufactured a working 1536px breakpoint. A class suffix is plain decimal,aspect-[…]emits its bracket verbatim as Tailwind does, and one fraction reader serves the sizing, position, flex and translate families, sotop-1/7andbasis-0/2read likew-1/7(#684, #689, #690, #696). object-[...]takes the whole CSS<position>grammar, soobject-[top],object-[left_top]andobject-[right_2rem]reach the sheet whereobject-[top]wroteobject-position: var(--top)(#734).- A negated arbitrary length works on every family and in every unit:
-mt-[2em]was an unknown class, as was every unit outside a handful (#735). theme(--x)and--theme(--x), v4's own spelling of a theme lookup, resolve in an arbitrary value, every namespace of the default theme answers, and atheme()naming a key the theme does not carry makes the class no utility, the way Tailwind emits no rule for it, a fallback argument standing in for the missing key.p-[theme(--spacing)]was an unknown class,rounded-[theme(--radius-lg)]wrote0px, andshadow-[0_0_0_1px_theme(a_b)]wrote the call through into the declaration. Atheme()alpha survives a hex-bound palette entry, and a resolved value keeps its underscores, so a palette entry bound tovar(--brand_red)no longer namesvar(--brand red)(#508, #687, #688, #701, #710).- An underscore that spells a name stays one. The argument of a
url(), the first argument of avar()or atheme(), and a\_escape all keep it, solist-image-[url(a_b.png)]andcontent-[url(a_b.png)]name their file,[--x:var(--my_var)]no longer references--my var,shadow-[0_0_0_var(--my_var)]is no longer truncated tovar(--my), andfont-['My\_Font']anddata-[foo=bar\_baz]:flexreach the sheet as written; the underscore outside them still becomes a space (#676, #688, #692, #695). - A closing bracket the value quotes or escapes belongs to the value, and a
url()is read whole by the CSS tokeniser:bg-[url(a\]b)]names the file the class means where it emittedurl("a\\]b"),bg-[url('a]b')],font-['My]Font'],[content:'a]b']andafter:content-['a]b']reach the sheet,mask-[url(x.png)_center]is refused rather than sliced into a selector no markup carries, and a string the value leaves open still refuses the class, as it does in Tailwind (#689, #692, #695). - An arbitrary value is spelled in the class name the way the author wrote it, so the selector matches the markup.
min-[0.5ch]:flexemitted.min-\[\.5ch\]\:flex, a rule nothing on the page could match, andbasis-[...],perspective-[...],underline-offset-[...]and annth-*argument keep their spelling too (#412, #413, #415, #489, #490, #532, #543, #564). - A class that cannot mean anything is refused at parse time, with the reason, instead of compiling to a dead selector or a declaration with no value. An invalid palette shade, a value the property cannot take, a bracket value that escapes its declaration, trailing text after the value, a colour channel with no byte, a non-canonical number, an unbalanced paren (
-left-[0)/*1]becameleft: calc(0 * -1)), a spacing step that is not a multiple of 0.25 on the inset and sizing families (w-1.7), and an unreadable animation, order, z-index, grid line,not-hasshorthand, data expression, gradient interpolation ornot-[...]content are all rejected, and a unitless decoration colour declares nothing rather than guessing (#127, #234, #237, #282, #284, #285, #307, #309, #405, #406, #407, #408, #410, #421, #422, #496, #532, #548, #691). - A bad arbitrary value no longer takes the run down. Six bracket spellings raised past the result-typed
of_string, nineto_stylesites answered an empty style instead of an error, and a candidate that raised while rendering aborted the run, so one class could crash the renderer or silently emit nothing (#144, #257, #266, #287). - Bracketed
has,group-hasandpeer-hasvariants keep Tailwind's:is(...)wrapper for bare type and complex selectors, and avar()reference is read to its end inside a bracket value, so one carrying its own parentheses or a fallback is not truncated (#564, #658).
Colours and effects
- An opacity modifier reaches every colour family. A ring, a ring offset, a per-side border, a shadow, a drop shadow, a decoration and a stroke all take one, the alpha can itself be a variable or a named
--opacity-*token in every one of them,currentcolorand a bracketvar()included (bg-cyan-400/(--my-alpha-value),shadow-red-500/half,decoration-current/(--o),ring-[var(--c)]/half,scrollbar-thumb-current/(--o),border-t-current/50,border-bs-[color:var(--c)]/50),transparentandinherittake one everywhere, andborder-inheritandborder-t-inheritcompile. Shadeless names such asshadow-white,stroke-white,ring-offset-whiteandring-offset-blackwork, a colour the project's@themedeclares names a ring, a ring offset and an inset ring as it names a shadow, a--text-shadow-color-*token names a text shadow's,light-dark()and an arbitrary shadow colour resolve, and a drop shadow keeps both of its default layers under an opacity (#169, #185, #201, #202, #209, #214, #225, #231, #244, #254, #281, #308, #322, #323, #813, #847, #859, #868, #876). - An opacity modifier over a bracket colour paints the colour the class named, as a
color-mix(), across all thirteen colour families.text-[rebeccapurple]/50and its siblings were unknown classes or rendered black;bg-[red]/50read the palette red-500 instead of CSS red; and elsewhere the mix resolved to that colour'soklab()channels rather than staying a mix, going out with no unguarded fallback where the alpha read a custom property, so a browser withoutcolor-mix()painted nothing.decoration-,divide-andstroke-accept the modifier at all now, a colour the browser resolves at use time keeps the@supportsfallback Tailwind writes, and a drop shadow's bracket names its colour as Tailwind reads it:drop-shadow-[#123456]/50anddrop-shadow-[color:var(--c)]/(--o)set--tw-drop-shadow-colorwhere the bracket was read as a size, and a named opacity on a drop-shadow size,drop-shadow-lg/half, names no utility (#508, #517, #711, #875). - A gradient stop's modifier is read as Tailwind reads it. A length or percentage stop is a position and takes none, so
from-[10px]/50is refused rather than mixed; a token-stream stop, a divide colour or a ring colour under a modifier reads the custom property in the unguarded fallback and the mix behind the guard, where tw folded it to100%, and a ring colour registers no@property(#877). - Tailwind's colour-mix polyfill applies to any declaration holding a
color-mix()that reads a custom property orcurrentcolor, wherever the mix stands: an arbitrary property, a bracket shadow, an inset shadow, a text shadow and a bracket image write the mix's first colour in the open and the value as written behind the guard, where tw wrote the mix alone (#878). - An arbitrary colour reaches CSS in the spelling the class wrote.
bg-[#f00]gave#ff0000,bg-[#ffffffff]gave#ffffffandbg-[#FF0000]lost its case, where Tailwind writes back what the bracket held (#700). - An arbitrary shadow keeps the colour the class named, and whatever the value reader accepted with it: a colour keyword,
currentcolor, a layer list, a leadinginset. A bracket naming a colour CSS knows came out asinset-shadow-noneunderinset-shadow-, and asshadow-noneunder an opacity modifier, soinset-shadow-[0_0_0_1px_red],shadow-[0_0_red]/50andinset-shadow-[0_0_0_1px_red]/50reach the sheet now. Where the alpha reads a custom property,shadow-andtext-shadow-keep the authored colour as the unguarded fallback instead of folding it through oklab at full opacity, which painted an opaque shadow in a browser with no relative colours (#711, #716, #717). Under a modifier the alpha replaces the colour's own, as Tailwind'soklab(from … / 50%)does, whereshadow-[0_1px_2px_#0000001a]/50kept the colour's 10%, and an arbitrary drop shadow takes the modifier too,drop-shadow-[0_1px_2px_red]/50was an unknown class.inset-shadow-[inset_0_1px_red], a bracket spelling theinsetthe utility supplies itself, is refused: Tailwind writesinset inset 0 1px red, which no browser draws, where tw drew the shadow the author did not get (#847). - A shadow list under a modifier is written the way Tailwind writes it, in every family: a
var()colour or avar()alpha keeps every layer's authored colour in the open and takes the alpha behind the relative colour guard, acurrentcolorlayer throughcolor-mix()behind its own guard nested inside, and a colour no hex spells takes the alpha in place of its own, where acolor-mix()multiplied the two. A trailingvar()is the colour in a project token as in a bracket, so--shadow-card: 0 1px 2px var(--c)paints with--crather than reading it as a spread. A project--text-shadow-<name>is a utility,--drop-shadow-<name>andshadow-innertake a modifier, a sized drop shadow reads its project override, andshadow-lg/[25]records the author's own number (#852). - A shadow read whole from a custom property takes a modifier in every family,
shadow-[var(--s)]/50,inset-shadow-[var(--s)]/50,drop-shadow-[var(--s)]/50andtext-shadow-[var(--s)]/50, under theshadow:hint too: the alpha channel is set and the value kept, as Tailwind writes it, where the class was refused or the modifier silently dropped.ring-[3px]declares the inset toggle and colour it reads, asring-2does, where the sheet dropped both reads; a width takes no opacity, andring-[3px]/50is refused (#853). - Palette box, inset-box and text shadows keep Tailwind's authored OKLCH value as their unguarded fallback instead of converting it to sRGB hex, and a gradient stop with an opacity keeps its theme colour as the palette declares it:
to-gray-950/40registered--color-gray-950as#030712, which moved every other use of the token on the page off the palette's oklch (#657, #822). --color-blackand--color-whiteare written#000and#fff, the three digits Tailwind spells them in, in the theme block and in every colour family's unguarded fallback (#711).
Variants
- Variants compose in the combinations Tailwind allows.
group-*andpeer-*take any state and a name (peer-checked/draft),has-*,group-has-*andpeer-has-*take any variant, a bare data attribute or a bracket selector (has-[a],has-peer-checked,group-has-data-[state=open]),not-*composes over any variant,in-*scopes to an ancestor in a given state (in-focus), a container query nests with the variants around it and reads a theme token (@min-[theme(--breakpoint-lg)]), a bracket@mediavariant wraps the utility in that query ([@media_print]:underline), and an arbitrary variant works with no&anchor ([code]:pr-4). A hover gate nests inside a wrapping media or container query, and an inner selector survives an at-rule variant such as[@supports(display:grid)]:grid. A variant over a media inner reaches the rule the inner query nests under its own gate,in-focus:md:hover:flexputting:where(:focus)in front of the class inside the hover media andhover:md:hover:flexits second:hover, where the nested rule kept the bare class; an ancestor variant puts the rest of a compound onto the class,:where(:focus) .x:hoverforin-focus:hover:flex; andgroup-not-[.a]:negates the bracket rather than losing the negation.before:andafter:over an inner variant nest as Tailwind nests them,contentdeclared once on.x::beforeunder no condition and the utility's declarations on.x::before<inner>under the inner's own at-rules, where the two were fused under the inner's condition with its selector dropped;marker:andselection:put the inner's selector after each pseudo-element; and@starting-stylenests with a container or supports query in either order (#167, #173, #196, #197, #198, #199, #200, #203, #204, #211, #213, #215, #219, #224, #231, #232, #233, #235, #238, #280, #314, #700, #814, #815, #850, #851, #855). not-negates what it wraps.not-[:target],not-[:nth-child(2)]andnot-[:has(.x)]negate the pseudo-class, where only nine were tabled and the rest negated a class literally named:target;not-[@supports(…)]negates the condition,not-[@supports(display:grid)]:flexwrapping the utility in@supports not (display:grid), where it negated the utility's own class; andhas-andnot-around an arbitrarydata-[…]oraria-[…]variant test the attribute, wherehas-data-[state=open]:ring-2read:has(.ring-2).not-@md:negates a container query as Tailwind 4.3.3 writes it,@container not (width >= 28rem), the container's name kept outside the negation, where the class was refused. A negated hover variant carries its@media not (hover: hover)twin whatever form the hover takes -not-group-hover:,not-peer-hover/x:,not-in-hover:,not-has-hover:- and the twin keeps the leaf the inner variants built, sonot-hover:focus:flexstyles.x:focuson a touch device;not-group-hover:flexemitted the selector half alone andnot-in-hover:negated the utility's own class.has-,in-and a secondnot-take any variant with a selector,has-not-focus:,in-group-hover:,in-nth-3:andnot-not-md:included. A compound condition, a negation with two halves or of a pure at-rule underhas-,in-ornot-, and a media or container inner inside a group or peer negation are refused, as Tailwind refuses them (#488, #493, #775, #803, #818, #850, #851).supports-[…]emits the test the author wrote, as a typed condition rather than a string reparsed after assembly: a bare property expands to a feature test,not(display:grid)becomes@supports not (display:grid)andselector(:has(a))passes through, where the first crashed with an uncaughtFailure(exit 125), and a malformed condition is refused (#135, #389, #484, #808).- A
!on a utility that writes rules of its own reaches their selectors, and marks the variables the utility sets.space-x-4!,container!,prose!andform-input!styled the class without the!, which no element carries, so nothing applied;shadow-md!left--tw-shadownormal, so a plainshadow-lgon the same element still chose the shadow drawn (#802, #807). - A variant no longer costs the rule inside it, or writes an empty one beside it. A hover gate survives an at-rule variant, a peer hover gate survives a selector variant, a variant stays wrapped around a
@starting-stylerule, a class a variant renames keeps the default transition theme, andsm:dark:hover:underlinenames its own class in the dark media block, where it emitted a declarationless.sm\:dark\:hover\:underline:hover {}beside a rule named.dark\:hover\:underline(#564, #703). - An opacity colour keeps its progressive-enhancement
@supportsguard when wrapped in a supports, container or starting-style variant. The moderncolor-mix()declaration was left unguarded inside that wrapper (#666).
Ordering and structure
- Utilities land where Tailwind puts them across the sheet. A colour's
@supportsrule stays with its fallback, container variants order by width, the logical sizing families sort last, line-clamp sorts with box-sizing, and isolation, float, clear, divide, masks, outline colours, the drop-shadow sizes, inset start and end, the basis fractions, border-spacing and the theme namespaces each follow Tailwind's order. An unvarianted utility comes before thenot-*group, and a variable whose slot was already taken is no longer dropped from the sheet (#242, #243, #249, #250, #251, #253, #263, #264, #267, #268, #269, #291, #292, #310, #311, #312, #670). - More property families emit in Tailwind's band: fill and stroke ahead of object-fit, aspect ratio before the dimensions, tab size inside typography, field sizing after display, logical block margins before the physical sides, ring widths in numeric order,
text-indentbeside the other text properties,text-wrap,overflow-wrap,word-breakandhyphensbefore white-space, the late typography colour block and the priority-7 theme tail in Tailwind's order,divide-x-reverse, container queries and column values where Tailwind puts them, and the transform, filter, gradient, gap, delay and list-style families throughout (#429, #443, #494, #523, #541, #552, #564). - Stacked and compound variants sort by what they contain rather than by their prefix text, from one table. A compound carries its inner value, a recursive compound follows its whole path, an arbitrary variant orders by its selector, data variants group by predicate, a negated breakpoint retains its responsive order, repeated element variants collapse to one key, and a
peer-variant sorts inside the peer group by the variant it wraps, asgroup-does: Tailwind writespeer-checked, thenpeer-hover, thenpeer-focus, wherepeer-hoversorted in front of every otherpeer-spelling. An unrecognised prefix used to return 0, which put the rule in a different sort bucket entirely rather than merely out of order (#520, #564, #672, #673, #773). - An opacity colour under
hover:keeps its@supportstwin beside its fallback, as Tailwind writes the pair. The twin sorted after every later utility of the group, sohover:bg-white/50came afterhover:text-white(#829). md:containerkeeps its breakpoint rules beside it, as Tailwind writes them, somd:max-w-2xlfollows them and wins on an element carrying both. The nested breakpoints sorted as a stacked variant, after every plain rule of themdblock (#833).- Container variants sort as Tailwind groups them: every
@max-*before every@*and@min-*, whatever value either names, and@lg:with@min-lg:in one container block.@lg:flex @max-lg:hiddenwrote the@lgblock first,@max-[theme(...)]came after@lg, and a stacked@sm:@max-md:sorted past@mdinstead of under@sm(#783, #827, #830). - Blocks group the way Tailwind groups them. Adjacent
@media,@containerand@supportsblocks with one condition are a single wrapper, at every level of nesting, sosm:p-4 sm:m-2is one breakpoint block andsm:dark:p-4 sm:dark:m-2one breakpoint block holding one dark block; a run of@starting-styleutilities emits as one block; a pseudo-element declares its content once,before:bg-red-500/50no longer repeating it in the@supportscolour twin; and a project's own utilities interleave with the built-in family writing the same property (#194, #220, #226, #228, #255, #256, #271, #313, #316, #319, #324, #668, #682, #726, #823).
OCaml API
- Typed construction covers clear, background attachment, clip, origin, position, repeat and size, outline width and colour, ring offsets, and background blend modes (#649).
- The typed
divideconstructors, fromdivide_xtodivide_style: only the two reverse utilities were exposed, so the rest of the family was reachable from a class string but not from OCaml.divide_x_lengthaccepts a line-width keyword, which the parser already did and the constructor raised on (#239, #522, closes #5). - The typed padding and gap constructors raise
Invalid_argumenton a negative size. Neither has a negative form, andp (-3)printedp-3, a different utility, with nothing to say so (#809). Var.needs_property_ruleanswersfalsefor a variable carrying metadata tw did not create, where it failed an assertion (#707).Modifiers.not_variant_orderandModifiers.prose_element_inner_selectorare gone: nothing read the first, and the second was the module's own helper. A caller of either usesModifiers.variant_order_of_prefixandModifiers.to_selector(#871).
Packaging
- Require cascade 1.2.0. While it remains unreleased, CI pins cascade's main branch so builds and tests follow upstream rather than an exact development revision (#297, #302, #305, #646).
- Follow cascade's grammar tightening.
-webkit-mask-clipand-webkit-mask-origincarry only the three boxes WebKit's own grammar has, somask-clip-fill,mask-clip-stroke,mask-clip-view,mask-no-clipand the threemask-origin-*siblings emit the unprefixed declaration alone, which is what every browser kept of the twin Tailwind writes.outline-[50%]emits itsoutline-stylealone, the declaration a browser keeps once it drops theoutline-width: 50%Tailwind writes beside it;border-[50%], whose Tailwind form isborder-color: 50%, is refused rather than emitting a liveborder-stylebehind a dropped width.list-[<name>]reads any<counter-style-name>, and a[color:theme(...)]whose alpha names no number is refused rather than resolved without it (#721).
1.0.0
- Initial public release candidate. Type-safe Tailwind CSS v4 in OCaml, with parity against the upstream v4 compiler (core utilities plus the official
formsandtypographyplugins).
sectionYPositions = computeSectionYPositions($el), 10)"
x-init="setTimeout(() => sectionYPositions = computeSectionYPositions($el), 10)"
>
On This Page