Skip to content

Glassmorphism Generator — Zig source

Create frosted-glass CSS - backdrop-blur, translucent background, subtle border, and soft shadow - with a copy-ready snippet and live preview.

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

//! =============================================================================
//! glassmorphism.zig — CosmoDev polyglot showcase port of the
//! `glassmorphism-generator` tool
//! -----------------------------------------------------------------------------
//! Language : Zig (0.13, standard library only)
//! Source:   ported from src/lib/glassmorphism.ts (the canonical, live
//!            TypeScript lib); mirrors
//!            src/tool-sources/glassmorphism-generator/{python.py,rust.rs}
//! License  : display source — part of CosmoDev's polyglot tool pages
//!            (dev.cosmolabs.org). Shown verbatim alongside the JS/TS/Go/Rust/
//!            Python ports and the other language ports.
//! -----------------------------------------------------------------------------
//! Builds a frosted-glass CSS block from blur / opacity / tint / radius /
//! border_opacity (+ optional saturation). Hex tints (#rgb / #rrggbb) are
//! validated by hand (no regex — stdlib only); opacity is clamped to [0,1];
//! the alpha channel is rounded with @floor(x*100 + 0.5)/100 — JS Math.round
//! (half-up) semantics — and numbers render like JS template literals
//! (10.0 -> "10", 0.3 -> "0.3") via jsNum() below. The output therefore
//! matches the live TypeScript lib byte-for-byte.
//!
//! All rendered strings are allocated with the allocator passed in; free them
//! individually or hand the allocator an arena (as main() does).
//! =============================================================================

const std = @import("std");

/// Configuration for one glassmorphism declaration block.
pub const GlassConfig = struct {
    blur: f64 = 0,
    opacity: f64 = 0,
    tint: []const u8 = "#ffffff",
    radius: f64 = 0,
    border_opacity: f64 = 0,
    /// null => 180.0 (lib default)
    saturation: ?f64 = null,
};

/// The rendered glassmorphism block plus its full CSS text.
pub const GlassCss = struct {
    background: []const u8,
    backdrop_filter: []const u8,
    webkit_backdrop_filter: []const u8,
    border: []const u8,
    border_radius: []const u8,
    box_shadow: []const u8,
    full_css: []const u8,
};

/// True when the byte is an ASCII hex digit (0-9, a-f, A-F).
fn isHexDigit(c: u8) bool {
    return (c >= '0' and c <= '9') or (c >= 'a' and c <= 'f') or (c >= 'A' and c <= 'F');
}

/// Parse one ASCII hex digit into 0..15; 0 on a non-digit.
fn hexVal(c: u8) u8 {
    if (c >= '0' and c <= '9') return c - '0';
    if (c >= 'a' and c <= 'f') return c - 'a' + 10;
    return c - 'A' + 10;
}

/// True when every byte is an ASCII hex digit.
fn isHexRun(s: []const u8) bool {
    if (s.len == 0) return false;
    for (s) |c| {
        if (!isHexDigit(c)) return false;
    }
    return true;
}

/// Render a float into `buf` the way JavaScript's template literal does —
/// shortest round-tripping decimal, no trailing ".0" on whole numbers.
/// Integer-valued floats go through an i64 first: `{d}` on a whole float can
/// keep a trailing ".0" depending on std.fmt version (the
/// css-animation-playground port casts for the same reason), while `{d}` on
/// an i64 is always plain digits. The range guard keeps @intFromFloat in
/// range; far beyond any blur/radius/saturation value.
fn jsNum(buf: []u8, x: f64) []const u8 {
    if (x == @trunc(x) and x > -9.2e18 and x < 9.2e18) {
        const as_int: i64 = @intFromFloat(x);
        return std.fmt.bufPrint(buf, "{d}", .{as_int}) catch "0";
    }
    return std.fmt.bufPrint(buf, "{d}", .{x}) catch "nan";
}

/// Parse #rgb or #rrggbb into {r, g, b}; null on a bad color.
/// A 3-char hex expands each digit (e.g. "abc" -> "aabbcc"), like CSS.
/// Exactly one leading '#' is stripped, matching the lib's /^#/ replace.
pub fn hexToRgb(hex: []const u8) ?[3]u8 {
    // Trim ASCII whitespace first — the set JS String.prototype.trim() strips.
    var h = std.mem.trim(u8, hex, " \t\n\x0b\x0c\r");
    if (h.len > 0 and h[0] == '#') h = h[1..];

    if (h.len == 3 and isHexRun(h)) {
        // "a" -> 0xaa — each digit doubles into a pair, like CSS.
        var out: [3]u8 = undefined;
        for (h, 0..) |c, i| out[i] = hexVal(c) * 16 + hexVal(c);
        return out;
    }
    if (h.len == 6 and isHexRun(h)) {
        return .{
            hexVal(h[0]) * 16 + hexVal(h[1]),
            hexVal(h[2]) * 16 + hexVal(h[3]),
            hexVal(h[4]) * 16 + hexVal(h[5]),
        };
    }
    return null;
}

/// Format rgba(), clamping + 2-decimal-rounding the alpha with JS Math.round
/// (half-up) semantics: @floor(x*100 + 0.5)/100. Caller owns the string.
pub fn rgba(gpa: std.mem.Allocator, r: u8, g: u8, b: u8, a: f64) ![]u8 {
    const c = @min(@max(a, 0), 1);
    var alpha_buf: [32]u8 = undefined;
    const alpha = jsNum(&alpha_buf, @floor(c * 100.0 + 0.5) / 100.0);
    return std.fmt.allocPrint(gpa, "rgba({d}, {d}, {d}, {s})", .{ r, g, b, alpha });
}

/// Build the full glassmorphism CSS for a config.
pub fn buildGlassmorphism(gpa: std.mem.Allocator, cfg: GlassConfig) !GlassCss {
    const blur = @max(cfg.blur, 0);
    const opacity = @min(@max(cfg.opacity, 0), 1);
    const radius = @max(cfg.radius, 0);
    const border_opacity = @min(@max(cfg.border_opacity, 0), 1);
    const saturation = cfg.saturation orelse 180;
    const rgb = hexToRgb(cfg.tint) orelse [3]u8{ 255, 255, 255 };

    var blur_buf: [32]u8 = undefined;
    var sat_buf: [32]u8 = undefined;
    var radius_buf: [32]u8 = undefined;
    const blur_s = jsNum(&blur_buf, blur);
    const sat_s = jsNum(&sat_buf, saturation);
    const radius_s = jsNum(&radius_buf, radius);

    const background = try rgba(gpa, rgb[0], rgb[1], rgb[2], opacity);
    const backdrop_filter = try std.fmt.allocPrint(
        gpa,
        "blur({s}px) saturate({s}%)",
        .{ blur_s, sat_s },
    );
    const border = try std.fmt.allocPrint(
        gpa,
        "1px solid {s}",
        .{try rgba(gpa, 255, 255, 255, border_opacity)},
    );
    const border_radius = try std.fmt.allocPrint(gpa, "{s}px", .{radius_s});
    const box_shadow = "0 8px 32px 0 rgba(31, 38, 135, 0.37)";

    // Both prefixed and unprefixed backdrop-filter carry the same value.
    const full_css = try std.fmt.allocPrint(gpa,
        \\background: {s};
        \\-webkit-backdrop-filter: {s};
        \\backdrop-filter: {s};
        \\border-radius: {s};
        \\border: {s};
        \\box-shadow: {s};
    , .{ background, backdrop_filter, backdrop_filter, border_radius, border, box_shadow });

    return .{
        .background = background,
        .backdrop_filter = backdrop_filter,
        .webkit_backdrop_filter = backdrop_filter,
        .border = border,
        .border_radius = border_radius,
        .box_shadow = box_shadow,
        .full_css = full_css,
    };
}

pub fn main() !void {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();

    const css = try buildGlassmorphism(arena.allocator(), .{
        .blur = 10,
        .opacity = 0.3,
        .tint = "#ffffff",
        .radius = 12,
        .border_opacity = 0.2,
    });

    const stdout = std.io.getStdOut().writer();
    try stdout.print("{s}\n", .{css.full_css});
}

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 →