Tailwind Compiler Zig

View Source

A Tailwind CSS v4-compatible compiler written in Zig. Accepts a list of CSS class candidate strings and returns minified production CSS. Everything happens in memory — no filesystem scanning, no external processes, no CLI.

[!IMPORTANT] This project is a maintained fork of BeaconCMS/tailwind_compiler, originally created by Brian Cardarella and developed by the Beacon CMS contributors. They built the core Zig compiler, Elixir NIF integration, utility and variant coverage, tests, benchmarks, WASM target, and release machinery. This fork retains their MIT license and copyright notice.

The fork provides an independently maintained Hex package for AshCms, with a separate OTP application, Elixir namespace, Zig package identity, NIF artifacts, and release stream. Fork-specific work includes candidate extraction from source and streams, root plugin-theme color preservation, and ongoing packaging and release maintenance. See NOTICE.md for the full attribution.

Try the fork's WASM Playground — compile Tailwind CSS entirely in your browser.

Performance

Compile-only benchmark against the Tailwind CSS v4.2.2 JS compile() API — same 2,980 candidates, no filesystem I/O on either side (Apple M4):

MetricZigTailwind v4 JS APIDifference
Avg compile time1.5 ms23 ms~15x faster
Median1.5 ms22 ms
Peak memory4.5 MB181 MB~40x less

Installation

Add to your mix.exs:

def deps do
  [{:tailwind_compiler_zig, "~> 1.0"}]
end

Precompiled NIF binaries are available for x86_64-linux, aarch64-linux, aarch64-macos, and x86_64-windows. The correct binary is downloaded automatically during mix compile — no Zig toolchain required.

WASM binary

A precompiled WebAssembly binary is also available with each release. This lets you run the full Tailwind compiler in any WASM runtime — browsers, Deno, Cloudflare Workers, etc.

To install the WASM binary during mix compile, set TAILWIND_COMPILER_ZIG_WASM_PATH to the directory or file path where it should be saved:

# Install to a directory (saved as tailwind_compiler_zig.wasm)
TAILWIND_COMPILER_ZIG_WASM_PATH=priv/static/assets mix compile

# Install to a specific file path
TAILWIND_COMPILER_ZIG_WASM_PATH=priv/static/assets/tw.wasm mix compile

The directory must already exist or compilation will fail. When unset, no WASM binary is downloaded.

The WASM binary exports three functions: alloc, free, and compile. See the WASM Playground source for a complete browser integration example.

Building from source

To compile from source instead of using a precompiled binary, add zigler to your dependencies and set the TAILWIND_COMPILER_ZIG_PATH environment variable:

def deps do
  [
    {:tailwind_compiler_zig, "~> 1.0"},
    {:zigler, "~> 0.16.0", runtime: false}
  ]
end
TAILWIND_COMPILER_ZIG_PATH=true mix compile

This requires Zig 0.16.0.

Elixir Usage

TailwindCompilerZig.compile(["flex", "p-4", "hover:bg-blue-500/50", "sm:text-lg"])
#=> {:ok, ".flex{display:flex}.p-4{padding:calc(var(--spacing)*4)}..."}

# Extract candidate strings from raw HTML/template source
TailwindCompilerZig.candidates(~s(<div class="flex p-4 hover:bg-blue-500/50"></div>))
#=> ["div", "flex", "p-4", "hover:bg-blue-500/50", "/div"]

# Extract candidates lazily from a file or IO stream
File.stream!("index.html", [], :line)
|> TailwindCompilerZig.candidates()
|> Enum.to_list()

# Compile raw HTML/template source by extracting candidates first
TailwindCompilerZig.compile_source(~s(<div class="flex p-4"></div>))
#=> {:ok, ".flex{display:flex}.p-4{padding:calc(var(--spacing)*4)}..."}

# compile_source/2 also accepts streams
File.stream!("index.html", [], :line)
|> TailwindCompilerZig.compile_source(preflight: false)

# Without preflight (base CSS reset)
TailwindCompilerZig.compile(["flex", "hidden"], preflight: false)

# With theme overrides (custom colors, spacing, fonts)
TailwindCompilerZig.compile(["text-brand", "p-4"],
  theme: ~s({"colors":{"brand":"#3f3cbb"},"spacing":"0.5rem"}))

# With custom CSS (plugins, user stylesheets)
TailwindCompilerZig.compile(["flex"],
  custom_css: ".custom-btn{background:blue;padding:1rem}")

# Tailwind v4 custom variants are registered from custom CSS
TailwindCompilerZig.compile(["hocus:underline"],
  custom_css: "@custom-variant hocus (&:hover, &:focus);")

# With plugin CSS (e.g., DaisyUI — extracts color variables and includes plugin CSS)
TailwindCompilerZig.compile(["bg-primary", "btn"],
  plugin_css: File.read!("path/to/daisyui.css"))

# Bang variant (raises on error)
css = TailwindCompilerZig.compile!(["flex", "p-4"])

The NIF runs on a dirty CPU scheduler. For a typical site (~3,000 candidates), expect ~1.5ms latency.

Zig Usage

const tailwind = @import("tailwind_compiler_zig");

const candidates = [_][]const u8{ "flex", "p-4", "hover:bg-blue-500/50", "sm:text-lg" };
const css = try tailwind.compile(allocator, &candidates, null, false, true, null, null, null);

Zig API

pub fn compile(
    alloc: std.mem.Allocator,
    candidates: []const []const u8,     // Tailwind class names
    theme_json: ?[]const u8,            // Optional JSON theme overrides
    include_preflight: bool,            // Include base CSS reset
    minify: bool,                       // true = minified, false = pretty-printed
    custom_css: ?[]const u8,            // Optional raw CSS to append
    custom_utilities_json: ?[]const u8, // Optional JSON mapping class names to CSS declarations
    plugin_css: ?[]const u8,            // Optional plugin CSS (e.g., DaisyUI output)
) ![]const u8

Building

Requires Zig 0.16.0 and Elixir 1.17+.

# Elixir
mix deps.get
mix compile
mix test

# Zig standalone
zig build test
zig build run
zig build -Doptimize=ReleaseFast

Feature Coverage

Static Utilities (~565)

Display, position, visibility, isolation, box-sizing, float, clear, overflow, overscroll, object-fit, pointer-events, resize, user-select, touch-action (composable), cursor, appearance, flex direction/wrap/grow/shrink, grid flow, justify/align/place content/items/self (including safe alignment), text alignment/decoration/transform/overflow/wrap, whitespace, word-break, hyphens, font style/variant/smoothing (composable), list style, vertical-align, background attachment/clip/origin/repeat/size/position, border style/collapse, outline, mix/bg blend mode, table layout, caption side, transitions, will-change, contain, forced-color-adjust, sr-only, field-sizing, scroll behavior/snap, break-after/before/inside, box-decoration, content-visibility, color-scheme, font-stretch, transform-style, backface-visibility, mask-clip/origin/mode/composite/type/repeat/size/position (with -webkit- prefixes), and more.

Functional Utilities (~85 roots)

  • Spacing: p-*, m-*, gap-*, inset-*, top/right/bottom/left-*, scroll-m*, scroll-p*, basis-*, mbs-*, mbe-*, pbs-*, pbe-*, mis-*, mie-* (logical properties)
  • Sizing: w-*, h-*, min-w/h-*, max-w/h-*, size-*, inline-*, block-*, min-inline/block-*, max-inline/block-* + viewport units (svw, lvw, dvw, svh, lvh, dvh, lh)
  • Colors: bg-*, text-*, border-*, accent-*, caret-*, fill-*, stroke-*, outline-color-*, decoration-*, shadow-color-*, divide-*, placeholder-* — all with opacity modifier support (bg-red-500/50 pre-resolved to #hex)
  • Typography: text-sm/lg/xl (font-size + line-height), font-sans/bold (family + weight), leading-*, tracking-*, font-weight-*
  • Borders: border-* (width + color), border-x/y/s/e/t/r/b/l-*, rounded-* (all corners), divide-x/y-*
  • Effects: shadow-*, inset-shadow-*, text-shadow-*, ring-*, inset-ring-*, ring-offset-*, opacity-* — composable box-shadow system
  • Filters: blur-*, brightness-*, contrast-*, grayscale, hue-rotate-*, invert, saturate-*, sepia + all backdrop-* — composable filter/backdrop-filter
  • Transforms: rotate-*, scale-*, translate-x/y/z-*, skew-x/y-*, rotate-x/y/z-*, scale-z-* — composable custom properties (2D + 3D)
  • Grid: cols-*/grid-cols-*, rows-*/grid-rows-*, col-span-*, col-start/end-*, row-span-*, row-start/end-*, auto-cols/rows-*
  • Gradients: bg-linear-to-*/bg-gradient-to-*, bg-radial-*, bg-conic-*, from-*, via-*, to-* — composable stops
  • Layout: aspect-*, columns-*, perspective-*, origin-*, container (responsive max-widths)
  • Transitions: duration-*, delay-*, ease-*, animate-*
  • Misc: z-*, order-*, line-clamp-*, content-*, list-*, outline-offset-*, underline-offset-*, grow-*, shrink-*, mask-image-*, border-spacing-*

Variants (77+)

  • Pseudo-classes: hover (with @media(hover:hover)), focus, focus-visible, focus-within, active, visited, target, first, last, only, odd, even, disabled, enabled, checked, required, valid, invalid, placeholder-shown, autofill, read-only, open, inert, and more
  • Pseudo-elements: before, after (with content injection), marker, selection, placeholder, file, backdrop, first-letter, first-line
  • Media: dark, print, motion-safe/reduce, contrast-more/less, portrait, landscape, forced-colors, inverted-colors, pointer-*, noscript
  • Responsive: sm, md, lg, xl, 2xl, max-*, min-*
  • Container: @sm, @md, @lg, @xl, @min-*, @max-*
  • Compound: group-*, peer-*, has-*, not-*, in-* (including group-aria-*, group-data-*)
  • Functional: aria-*, data-*, supports-*, nth-*, nth-last-*, nth-of-type-*, nth-last-of-type-*
  • Other: ltr, rtl, starting, *, **, arbitrary [&>svg]

Infrastructure

  • Composable @property declarations for shadow, ring, translate, scale, gradient, filter, backdrop-filter, font-variant-numeric, border-spacing
  • @keyframes for built-in animations (spin, ping, pulse, bounce)
  • @layer theme with tree-shaken CSS variables (only emits what's used)
  • @layer base with Tailwind v4 preflight
  • CSS.escape() spec-compliant selector escaping
  • Pre-computed oklch→sRGB color conversion (288-color lookup table from LightningCSS)
  • Arena allocator — one bulk deallocation per compile call
  • Arbitrary values: bg-[#0088cc], w-[calc(100%-2rem)], [color:red]
  • Theme function shorthand: bg-(--my-color)var(--my-color)
  • Negative utilities: -mt-4, -rotate-12, -translate-y-2
  • Important modifier: flex!!important
  • Fraction values: w-1/250%
  • Custom CSS passthrough: append plugin CSS, user stylesheets, or custom components
  • JSON theme overrides: custom colors, spacing, fonts merged with defaults

Benchmark

# Run 10-round benchmark (default)
mix run benchmark/benchmark.exs

# Custom rounds
BENCH_ROUNDS=20 mix run benchmark/benchmark.exs

# Include preflight CSS
BENCH_PREFLIGHT=1 mix run benchmark/benchmark.exs

Architecture

lib/
  tailwind_compiler_zig.ex       Elixir public API
  tailwind_compiler_zig/nif.ex   Zigler NIF wrapper (dirty CPU scheduler)

src/
  root.zig            Zig public API: compile()
  compiler.zig        Context, dedup, sort, container responsive, @property/@keyframes
  candidate.zig       Bracket-aware parser, findRoots, modifiers, arbitrary values
  utilities.zig       ~350 static + ~85 functional utility handlers
  variants.zig        77+ variant definitions, ordering, and application
  emitter.zig         CSS.escape(), minified output, @layer/@property/@keyframes
  color.zig           oklchsRGB conversion, hex8 formatting, 288-color lookup
  theme.zig           JSON parsing, variable resolution, usage tracking
  default_theme.zig   Complete Tailwind v4 default theme (colors, spacing, etc.)

test/
  tailwind_compiler_zig_test.exs   ExUnit tests

benchmark/
  run_benchmark.sh        10-round benchmark runner
  generate_pages.py       Parametric HTML page generator
  bench_zig.zig           Zig benchmark binary with μs-precision timing
  results/report.html     Interactive results dashboard

License and attribution

MIT. The original copyright notice for Brian Cardarella is preserved in the license file. See NOTICE.md for the upstream contribution and fork relationship.