tw — Type-safe Tailwind CSS v4 in OCaml
tw is an OCaml implementation of Tailwind CSS v4. You assemble utilities as ordinary OCaml values and compile them to CSS through the same typed variable-and-layer pipeline Tailwind uses, producing byte-for-byte identical output. Unknown utilities are caught at compile time, and nothing depends on Node.js, PostCSS, or the Tailwind CLI at run time.
open Tw
(* A set of utilities is a value of type [Tw.t list]. *)
let card =
[
flex; flex_col; gap 4; p 6;
bg white; rounded_lg; shadow_sm;
border; border_color ~shade:200 gray;
hover [ shadow_md ];
dark [ bg ~shade:800 gray; text ~shade:100 gray ];
]
Features
- Tailwind v4 parity — output is checked byte-for-byte against the real Tailwind CSS CLI across the upstream utility and variant test suites.
- Both official plugins —
@tailwindcss/typography (prose) and @tailwindcss/forms are fully implemented. - Type-safe — utilities are typed constructors, so invalid combinations are compile errors rather than silent no-ops.
- String parsing —
Tw.str "flex p-4 bg-blue-500" turns class strings into typed utilities for dynamic class lists. - Pure OCaml — no JavaScript toolchain required; the library also compiles to JavaScript with js_of_ocaml.
Installation
Requires OCaml >= 5.2.
opam install tw
Usage
Typed utilities
open Tw
(* Spacing scales: the integer is multiplied by 0.25rem, so [p 4] is 1rem. *)
let layout = [ flex; items_center; gap 4; p 6; mx_auto; max_w_4xl ]
(* Colors default to shade 500; [~shade] picks another, [~opacity] adds alpha. *)
let colors = [ bg blue; text white; border_color ~shade:300 gray; bg ~opacity:50 white ]
(* Variants nest as functions: responsive breakpoints, state, and dark mode. *)
let responsive =
[ p 4; md [ p 8 ]; lg [ p 12 ]; bg blue; hover [ bg ~shade:600 blue ]; dark [ text ~shade:300 gray ] ]
(* The typography plugin, including per-element variants. *)
let article =
[ prose; prose_lg; mx_auto; prose_headings [ text ~shade:600 blue ]; prose_code [ bg ~shade:100 gray ] ]
Rendering to CSS
open Tw
(* [to_classes] returns the value for an HTML [class] attribute. *)
let class_attr = to_classes [ flex; p 4; bg blue ]
(* [to_css] builds a stylesheet; [Css.to_string] renders it to text. *)
let stylesheet = to_css [ flex; p 4; bg blue; hover [ bg ~shade:600 blue ] ]
let css = Css.to_string ~minify:true stylesheet
Parsing class strings
(* [Tw.str] parses a class string into utilities (raises on unknown classes). *)
let styles = Tw.str "flex items-center gap-4 p-6 bg-white rounded-lg shadow-sm"
(* [Tw.of_string] parses a single class and returns a result. *)
let parsed = Tw.of_string "hover:bg-blue-600" (* Ok _ *)
let rejected = Tw.of_string "not-a-class" (* Error (`Msg _) *)
CLI
# Generate CSS for specific classes
tw -s "flex p-4 bg-blue-500 hover:bg-blue-600"
# Scan files for classes
tw src/ > styles.css
# Compare with real Tailwind CSS
tw -s "prose mb-4 dark:text-gray-300" --diff
Supported Utilities
All Tailwind CSS v4 core utilities and both official plugins:
Category | Examples |
|---|
Layout | flex, grid, block, hidden, container
|
Spacing | p-4, mx-auto, gap-4, space-x-2.5
|
Sizing | w-full, h-screen, size-4, max-w-4xl
|
Typography | text-sm, font-bold, leading-tight, tracking-wide
|
Colors | bg-blue-500, text-white, border-gray-300
|
Opacity | bg-blue-500/50, text-white/80
|
Borders | border, rounded-lg, divide-x, ring-2
|
Effects | shadow-sm, opacity-50, blur-lg
|
Transforms | scale-50, rotate-45, translate-x-1/2
|
Transitions | transition-colors, duration-200, ease-in
|
Interactivity | cursor-pointer, select-none, scroll-smooth
|
Filters | backdrop-blur, grayscale, invert
|
Tables | table-auto, border-collapse, border-spacing-4
|
Prose | prose, prose-lg, prose-sm, prose-headings:, prose-pre:
|
Forms | form-input, form-select, form-checkbox
|
Variants
Variant | Example |
|---|
Responsive | sm:, md:, lg:, xl:, 2xl:
|
State | hover:, focus:, active:, disabled:
|
Dark mode | dark:
|
Group/Peer | group-hover:, peer-checked:
|
Pseudo-elements | before:, after:, marker:, placeholder:
|
Prose elements | prose-headings:, prose-p:, prose-a:, prose-code:
|
Stacked | dark:hover:, md:focus:, sm:dark:hover:
|
Development
Prerequisites
The --diff and --tailwind CLI modes require the tailwindcss standalone binary for comparison testing:
# macOS (Apple Silicon)
curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-macos-arm64
chmod +x tailwindcss-macos-arm64
sudo mv tailwindcss-macos-arm64 /usr/local/bin/tailwindcss
# Linux (x64)
curl -sLO https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64
chmod +x tailwindcss-linux-x64
sudo mv tailwindcss-linux-x64 /usr/local/bin/tailwindcss
Building
dune build # build the library and CLI
dune runtest # run all tests, including README examples
# Compare a single utility with Tailwind
dune exec -- tw -s "bg-blue-500 hover:bg-blue-600" --diff
License
ISC — see LICENSE.md.