A clean-room Sass compiler written in Zig for native and Zig build-tooling workflows. zsass provides a standalone CLI and an embeddable Zig API, with no Dart Sass or libsass runtime dependency.
zsass is an early v0.1 clean-room Sass compiler written in Zig.
Dart Sass is already fast and mature. zsass is not positioned as a revolutionary speedup or a universal drop-in replacement. The goal is to provide a native, embeddable Sass implementation that fits naturally into Zig and native build-tooling workflows.
zsass passes the pinned sass-spec suite used in this repository, but
this is not a full compatibility guarantee. Real-world stylesheets may
still expose differences from Dart Sass. If you find a divergence,
please open a compatibility report
with a minimal SCSS reproducer.
zsass may be interesting if you:
- want a Sass compiler that can be built and embedded from Zig;
- want a standalone native binary for build pipelines;
- are building native CSS tooling;
- are interested in compiler implementation techniques;
- want to test real-world Sass compatibility against a clean-room implementation.
If you only need a mature, widely deployed Sass compiler today, Dart Sass remains the default recommendation.
The installer detects your OS / arch, downloads the matching tarball
from GitHub Releases, verifies its SHA256, and drops the binary into
~/.local/bin (Unix) or %LOCALAPPDATA%\zsass\bin (Windows).
Linux / macOS:
curl -fsSL https://raw.githubusercontent.com/nihen/zsass/main/scripts/install.sh | shWindows (PowerShell):
iwr -useb https://raw.githubusercontent.com/nihen/zsass/main/scripts/install.ps1 | iexPin a version or change the prefix:
curl -fsSL https://raw.githubusercontent.com/nihen/zsass/main/scripts/install.sh \
| sh -s -- --version v0.1.0 --prefix ~/opt/zsassIf you would rather audit the installer first, the scripts live at
scripts/install.sh /
scripts/install.ps1.
brew install nihen/tap/zsassThe formula lives in nihen/homebrew-tap
and is regenerated on every release by
.github/workflows/release.yml.
Each upgrade reuses the same SHA256 sidecars that the release workflow
publishes alongside the archives.
Release images are published to GitHub Container Registry:
docker run --rm --user "$(id -u):$(id -g)" -v "$PWD":/work -w /work ghcr.io/nihen/zsass:latest input.scss output.cssFor reproducible builds, pin the release tag instead of latest:
docker run --rm ghcr.io/nihen/zsass:0.3.5 --versionThe --user flag keeps generated files owned by your host user on
Linux. The image is built from the same Linux release archives that are
published on GitHub Releases. The Docker build verifies the downloaded
archive against its .sha256 sidecar before extraction; Sigstore
provenance materials (.sig / .pem) are still published with each
release for separate provenance checks.
Each release publishes tar.gz archives for Linux x86_64 / aarch64 and
macOS x86_64 / aarch64, plus a zip for Windows x86_64. Every asset
has a sibling .sha256 file (and a .sig / .pem for Sigstore-aware
setups).
sha256sum -c zsass-v0.1.0-linux-x86_64.tar.gz.sha256
tar -xzf zsass-v0.1.0-linux-x86_64.tar.gz
install -m 0755 zsass-v0.1.0-linux-x86_64/zsass ~/.local/bin/For a stronger provenance check than SHA256 -- typically only worth
wiring up in unattended / CI / production setups -- verify the Sigstore
signature with cosign:
cosign verify-blob \
--certificate zsass-v0.1.0-linux-x86_64.tar.gz.pem \
--signature zsass-v0.1.0-linux-x86_64.tar.gz.sig \
--certificate-identity 'https://github.com/nihen/zsass/.github/workflows/release.yml@refs/tags/v0.1.0' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
zsass-v0.1.0-linux-x86_64.tar.gzVerified OK proves the artifact came from this repository's release
workflow. The signing material lives in the public Sigstore Rekor
transparency log - no secret keys are involved.
zig build -Doptimize=ReleaseFast # binary at zig-out/bin/zsass
zig build install --prefix ~/.local # install to ~/.local/binFor repeatable CI builds the repo also ships
scripts/install_cli.sh (Linux / macOS) and
scripts/install_cli.ps1 (Windows), which
honour ZSASS_INSTALL_PREFIX / ZSASS_INSTALL_OPTIMIZE /
ZSASS_GLOBAL_CACHE_DIR. See docs/cli.md for details.
# Compile SCSS to CSS (file-to-file; also writes input.css.map by default)
zsass input.scss output.css
# Stdin/stdout
echo 'a { b: 1 + 2; }' | zsass --stdin
# Directory compilation
zsass src/styles/:dist/css/
# Diagnostics
zsass --version
zsass --infoNotes:
- stdin is explicit: pass
--stdinor-when piping input - file outputs enable detached source maps by default; stdout keeps source
maps off unless you request
--embed-source-map
Add zsass as a dependency (writes the entry into your build.zig.zon):
zig fetch --save=zsass \
https://github.com/nihen/zsass/archive/refs/tags/v0.1.0.tar.gzThen wire it up in build.zig:
const zsass_dep = b.dependency("zsass", .{ .target = target, .optimize = optimize });
exe.root_module.addImport("zsass", zsass_dep.module("compiler"));// usage
const std = @import("std");
const zsass = @import("zsass");
pub fn buildCss(alloc: std.mem.Allocator, source: []const u8) ![]u8 {
return try zsass.compileSourceToCss(alloc, source, "<stdin>", &.{}, .{});
}The trailing CompileOptions lets you pick a different output style or
suppress @warn / @debug:
const css = try zsass.compileSourceToCss(alloc, source, "<stdin>", &.{}, .{
.output_style = .compressed,
.quiet = true,
});For source-map output use compileSourceToCssWithSourceMap, which returns a
CompileCssWithSourceMapResult owning both the CSS and the source-map JSON.
For multi-file batches the embedding surface also exposes a parallel
compileFiles, which mirrors the CLI's worker pool / shared source &
parsed-AST caches but writes results into in-memory CompileFileResult
records:
const results = try zsass.compileFiles(alloc, paths, .{
.output_style = .compressed,
.source_map = false,
.quiet = true,
.load_paths = &.{ "shared/styles", "vendor/sass" },
.jobs = 0, // 0 = std.Thread.getCpuCount()
});
defer {
for (results) |*r| r.deinit(alloc);
alloc.free(results);
}When the build is configured with -Dprofile=true, embedders can dump the
aggregated perf counters via zsass.dumpProfile() (no-op otherwise).
See docs/api.md for the full embedding surface and examples/ for runnable samples.
This release passes the upstream sass-spec suite vendored at the
submodule pin in tests/sass-spec: 13,885 / 13,885 cases pass, 6
skipped (see tests/sass-spec/.gitmodules for the exact commit and
tests/spec_runner.zig for the runner). This validates behavior
against the cases the suite covers; outside that, real-world
stylesheets may still diverge from Dart Sass. Re-run with
zig build test after updating the submodule to verify the pin
yourself, and please file a compatibility report
if you hit a divergence.
zsass ships reproducible benchmark scripts. On the benchmark set used in this repository it is competitive with Dart Sass and faster in some cases, but performance depends on workload, machine, and build mode. Treat the numbers as a starting point and rerun on your own setup.
Wall-clock time compiling the upstream sass-spec suite end-to-end
(scripts/bench.sh, ReleaseFast, batch mode, single process):
| Suite (entries) | zsass | dart-sass | ratio |
|---|---|---|---|
| sass-spec (~13400) | 3865 ms | 10323 ms | 0.37x |
Measured on 2026-05-08, Linux x86_64 (zsass ReleaseFast vs
sass --no-source-map from the bundled dart-sass release). Numbers
will drift as both compilers evolve and depend on CPU / OS / Zig
optimize mode.
git submodule update --init # fetch sass-spec (required for spec tests)
zig build # build CLI
zig build unit-test # unit tests only (fast)
zig build test # unit tests + sass-spec
zig build spec # sass-spec only (with --filter, --quiet options)
zig build realworld # external fixture runner; defaults to all suites
# under ../zsass-realworld-fixtures (see docs)Embedding examples (also handy as smoke tests):
zig build api-example # in-memory compile demo (examples/embed_basic.zig)
zig build api-file-example # file-based compile demo (examples/embed_file.zig)
zig build api-files-example # parallel batch compile demo (examples/embed_files.zig)
zig build api-smoke # all three examples back-to-back (regression check)
zig build quickstart # CLI smoke + api-smoke (fastest "is it wired?" check)See docs/realworld-fixture.md for the external-fixture workflow used for large real-world compatibility snapshots.
src/
main.zig # CLI entry point (delegates to runtime/driver.zig)
api.zig # Public embedding API (compileSourceToCss*)
frontend/ # Lexer + parser (.scss / .sass) -> flat AST
lexer.zig
parser.zig
ast_flat.zig
sass_converter.zig
resolve/ # AST -> ResolvedProgram (scope, @use/@forward,
# @import expansion, mixin/function decls)
resolver.zig # main entry; statement handlers split into stmt_*.zig
data.zig # shared records (Resolved AST, ModuleResolver, ...)
module_*.zig # @use / @forward / @import clusters
stmt_*.zig # @media / @at-root / @content / ... handlers
ir/ # ResolvedProgram -> bytecode Program
compiler.zig
opcode.zig
rule_ir.zig # the Rule IR the VM appends to
source_map.zig # SourceMap v3 emitter
runtime/ # VM + driver + I/O
vm.zig # bytecode dispatcher; appends Rule IR
driver.zig # CLI driver (stdin / file / dir / watch / REPL)
io.zig # threaded std.Io facility shared by callers
selector/ # Selector model + @extend unification
selector.zig
extend.zig
builtin/ # sass:math / sass:color / sass:string / sass:list
# / sass:map / sass:meta / sass:selector
color/ # color value model + format dispatch
tests/
spec_runner.zig # sass-spec HRX test runner
sass-spec/ # git submodule (official test suite)
docs/
api.md # Embedding API guide
cli.md # CLI reference
ci.md # CI/CD setup
realworld-fixture.md # External fixture workflow
examples/
embed_basic.zig # In-memory compile + source map demo
embed_file.zig # File-based compile demo
embed_files.zig # Parallel batch compile + compressed output demo
sample.scss # Sample stylesheet (input for embed_file.zig)
Pipeline:
.scss / .sass -> Lexer -> Parser -> Resolve -> Compile (bytecode)
-> VM (appends Rule IR) -> Writer (1-pass + optional source map)
-> CSS (+ .map)
zsass follows a clean-room policy: Dart Sass source code, vendored
copies, decompiled artifacts, and AI-generated derivatives of Dart Sass
source must not be used. Behavior is validated through sass-spec,
Sass documentation, and the
observable output of the official sass CLI.
AI coding assistants may be used as part of the development workflow; the maintainer is responsible for code review, tests, and releases.