Skip to content

Box-Shadow Generator — Zig source

Design layered CSS box-shadows with offset, blur, spread, color, and inset. Live preview and copy-ready CSS.

This is the Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

// box-shadow-generator — Zig polyglot showcase port.
//
// Pure CSS box-shadow builder. Formats one or more shadow layers and joins
// them into a single CSS box-shadow value. Deterministic, dependency-free,
// and never panics on bad input: invalid colors quietly fall back to a
// neutral translucent black, so a single bad color never breaks the whole
// stack.
//
// This is the Zig sibling of src/lib/boxShadow.ts (the canonical TypeScript
// that powers the live tool). The public surface mirrors the TS: a
// ShadowLayer struct plus parseColor, formatLayer, and buildBoxShadow
// (Zig convention keeps snake_case, matching the TS names directly). Zig
// ships no regex engine, so the color-shape checks are written by hand
// exactly like the Rust sibling — they accept what the TS regexes do,
// nothing more. Functions that build strings take an allocator; on
// allocation failure they degrade to a static fallback rather than
// bubbling an error, keeping the "total over arbitrary input" contract.
//
// Language: Zig 0.13 (standard library only).
// Ported from src/lib/boxShadow.ts.
// Source: CosmoDev polyglot showcase port.
// License: display source — part of CosmoDev's polyglot tool pages.

const std = @import("std");

/// A single layer in a CSS box-shadow stack.
pub const ShadowLayer = struct {
    inset: bool = false,     // draw the shadow inside the box
    offset_x: f64 = 0,       // horizontal offset in px
    offset_y: f64 = 0,       // vertical offset in px
    blur: f64 = 0,           // blur radius in px
    spread: f64 = 0,         // spread distance in px
    color: []const u8 = "",  // any CSS color (named, hex, rgb(), hsl(), ...)
};

/// Outcome of validating a color string. `err` is null when `ok` is true,
/// mirroring the TypeScript `{ ok: boolean; error: string | null }` shape.
/// When non-null it points at the allocator-owned message (or a static
/// fallback if allocation failed).
pub const ColorResult = struct {
    ok: bool,
    err: ?[]const u8,
};

/// CSS named colors accepted without further inspection. parseColor()
/// lower-cases its input first, so the comparison is effectively
/// case-insensitive.
const named_colors = [_][]const u8{
    "transparent", "black", "white", "red", "green", "blue", "yellow",
    "orange", "purple", "pink", "gray", "grey", "brown", "cyan", "magenta",
};

fn isNamedColor(c: []const u8) bool {
    for (named_colors) |name| {
        if (std.mem.eql(u8, c, name)) return true;
    }
    return false;
}

/// Matches the TS `^#[0-9a-f]{3}([0-9a-f]{3})?$` and `^#[0-9a-f]{8}$` rules:
/// a leading '#' followed by exactly 3, 6, or 8 lower-case hex digits.
/// `s` must already be lower-cased, as in the TS original.
fn isHexColor(s: []const u8) bool {
    if (s.len < 2 or s[0] != '#') return false;
    const n = s.len - 1;
    if (n != 3 and n != 6 and n != 8) return false;
    for (s[1..]) |c| {
        const digit = c >= '0' and c <= '9';
        const lower_hex = c >= 'a' and c <= 'f';
        if (!digit and !lower_hex) return false;
    }
    return true;
}

/// Matches the TS `^rgba?\([^)]+\)$` / `^hsla?\([^)]+\)$` rules: a function
/// name, an opening paren, one or more characters that are not ')', and a
/// closing paren at the very end. The inner contents are otherwise
/// unchecked, matching the live tool's permissive behavior. `prefixes`
/// should list the longer form first (e.g. "rgba" before "rgb") so each is
/// tried independently.
fn isFunctionColor(s: []const u8, prefixes: []const []const u8) bool {
    for (prefixes) |prefix| {
        if (!std.mem.startsWith(u8, s, prefix)) continue;
        const rest = s[prefix.len..];
        // Need '(' ... ')' with at least one char strictly between. The TS
        // `[^)]+` forbids any ')' inside; combined with the trailing `)$`
        // anchor, the closing paren must be the final character.
        if (rest.len >= 3 and rest[0] == '(' and rest[rest.len - 1] == ')') {
            const inside = rest[1 .. rest.len - 1];
            if (inside.len > 0 and std.mem.indexOfScalar(u8, inside, ')') == null) {
                return true;
            }
        }
    }
    return false;
}

/// The whitespace set Python's `str.strip()` removes (ASCII whitespace).
fn isSpace(c: u8) bool {
    return c == ' ' or c == '\t' or c == '\n' or c == '\r' or c == 0x0b or c == 0x0c;
}

/// Copy `s` trimmed of whitespace and lower-cased into `buf`, returning the
/// canonical slice — the `.strip().lower()` parseColor applies once up front.
fn lowerTrim(buf: []u8, s: []const u8) []const u8 {
    var start: usize = 0;
    var end: usize = s.len;
    while (start < end and isSpace(s[start])) start += 1;
    while (end > start and isSpace(s[end - 1])) end -= 1;
    var n: usize = 0;
    for (s[start..end]) |c| {
        if (n == buf.len) break;
        buf[n] = std.ascii.toLower(c);
        n += 1;
    }
    return buf[0..n];
}

/// The shape checks shared by parseColor (for its result) and normalizeColor
/// (for its fallback decision). `c` must already be lower-cased and trimmed.
fn colorMatches(c: []const u8) bool {
    if (isNamedColor(c)) return true;
    if (isHexColor(c)) return true;
    if (isFunctionColor(c, &[_][]const u8{ "rgba", "rgb" })) return true;
    if (isFunctionColor(c, &[_][]const u8{ "hsla", "hsl" })) return true;
    return false;
}

/// Validate a CSS color string. Accepts the curated named-color set plus
/// hex (#rgb, #rrggbb, #rrggbbaa), rgb()/rgba(), and hsl()/hsla() forms.
/// The functional notations are checked for well-formed wrappers only, not
/// their numeric contents. `allocator` is used only to build the error
/// message; if that allocation fails the static text "invalid color" is
/// returned in its place.
pub fn parseColor(allocator: std.mem.Allocator, color: []const u8) ColorResult {
    // Lower-case + trim once so every shape check below sees a canonical form.
    var buf: [256]u8 = undefined;
    const c = lowerTrim(&buf, color);
    if (c.len == 0) return .{ .ok = false, .err = "empty color" };
    if (colorMatches(c)) return .{ .ok = true, .err = null };
    const msg = std.fmt.allocPrint(
        allocator,
        "invalid color: {s}",
        .{color},
    ) catch "invalid color";
    return .{ .ok = false, .err = msg };
}

/// Keep a color when it parses and otherwise substitute a neutral
/// translucent black. This is what makes buildBoxShadow total over
/// arbitrary input. No allocation: the trimmed slice borrows from `color`.
fn normalizeColor(color: []const u8) []const u8 {
    var buf: [256]u8 = undefined;
    const valid = blk: {
        const c = lowerTrim(&buf, color);
        break :blk colorMatches(c);
    };
    const ws = " \t\n\r\x0b\x0c";
    return if (valid) std.mem.trim(u8, color, ws) else "rgba(0,0,0,0.5)";
}

/// Render an f64 the way JavaScript's template literals would: whole
/// numbers drop the decimal point (5, not 5.0); `{d}` keeps the short
/// decimal form for fractions (5.5 -> "5.5").
fn formatNumber(allocator: std.mem.Allocator, v: f64) []const u8 {
    if (v == @trunc(v) and std.math.fabs(v) < 1e15) {
        const whole: i64 = @intFromFloat(v);
        return std.fmt.allocPrint(allocator, "{d}", .{whole}) catch "0";
    }
    return std.fmt.allocPrint(allocator, "{d}", .{v}) catch "0";
}

/// Render one shadow layer as its CSS fragment, e.g.
/// "inset 4px 8px 16px 0px #1a2b3c" or "0px 2px 4px 0px rgba(0,0,0,0.5)".
/// The returned slice is owned by `allocator`.
pub fn formatLayer(allocator: std.mem.Allocator, layer: ShadowLayer) []const u8 {
    return std.fmt.allocPrint(allocator, "{s}{d}px {d}px {d}px {d}px {s}", .{
        if (layer.inset) "inset " else "",
        layer.offset_x,
        layer.offset_y,
        layer.blur,
        layer.spread,
        normalizeColor(layer.color),
    }) catch "";
}

/// Compose a full CSS box-shadow declaration from an ordered list of layers
/// (the first layer renders on top). An empty slice yields the CSS keyword
/// "none", matching the property's default value. The returned slice is
/// owned by `allocator`.
pub fn buildBoxShadow(allocator: std.mem.Allocator, layers: []const ShadowLayer) []const u8 {
    if (layers.len == 0) return "none";
    var out = std.ArrayList(u8).init(allocator);
    for (layers, 0..) |layer, i| {
        if (i > 0) out.appendSlice(", ") catch return "";
        out.appendSlice(formatLayer(allocator, layer)) catch return "";
    }
    return out.toOwnedSlice() catch "";
}

Also available in 13 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →