Packages
tailwind_compiler_zig
1.0.0
A maintained Zig-based Tailwind CSS v4 compiler for Elixir, forked from BeaconCMS/tailwind_compiler.
Current section
Files
Jump to
Current section
Files
tailwind_compiler_zig
README.md
README.md
# Tailwind Compiler Zig
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](https://github.com/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](NOTICE.md) for the full attribution.
**[Try the fork's WASM Playground](https://olivermt.github.io/tailwind_compiler_zig/)** — 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):
| Metric | Zig | Tailwind v4 JS API | Difference |
|--------|-----|-------------------|------------|
| Avg compile time | **1.5 ms** | 23 ms | **~15x faster** |
| Median | **1.5 ms** | 22 ms | |
| Peak memory | **4.5 MB** | 181 MB | **~40x less** |
## Installation
Add to your `mix.exs`:
```elixir
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:
```bash
# 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](https://olivermt.github.io/tailwind_compiler_zig/) 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:
```elixir
def deps do
[
{:tailwind_compiler_zig, "~> 1.0"},
{:zigler, "~> 0.16.0", runtime: false}
]
end
```
```bash
TAILWIND_COMPILER_ZIG_PATH=true mix compile
```
This requires [Zig 0.16.0](https://ziglang.org/download/).
## Elixir Usage
```elixir
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
```zig
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
```zig
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](https://ziglang.org/download/) and [Elixir 1.17+](https://elixir-lang.org/install.html).
```bash
# 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/2` → `50%`
- Custom CSS passthrough: append plugin CSS, user stylesheets, or custom components
- JSON theme overrides: custom colors, spacing, fonts merged with defaults
## Benchmark
```bash
# 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 oklch→sRGB 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](https://github.com/olivermt/tailwind_compiler_zig/blob/main/LICENSE).
See [NOTICE.md](NOTICE.md) for the upstream contribution and fork relationship.