Skip to content

Box-Shadow Generator — C source

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

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

/*
 * box-shadow-generator — C 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 and never crashes
 * 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 C sibling of src/lib/boxShadow.ts (the canonical TypeScript
 * that powers the live tool). The public surface mirrors the TS: a
 * shadow_layer struct plus parse_color, format_layer, and build_box_shadow.
 * The C standard library ships no regex engine (POSIX <regex.h> is not ISO
 * C), so the color-shape checks are written by hand exactly like the Rust
 * sibling — they accept what the TS regexes do, nothing more.
 *
 * Language: C11 (standard library only).
 * Ported from src/lib/boxShadow.ts.
 * Source: CosmoDev polyglot showcase port.
 * License: display source — part of CosmoDev's polyglot tool pages.
 */

#include <ctype.h>
#include <math.h>
#include <stdio.h>
#include <string.h>

/* A single layer in a CSS box-shadow stack. */
typedef struct shadow_layer {
    int    inset;     /* draw the shadow inside the box */
    double offset_x;  /* horizontal offset in px */
    double offset_y;  /* vertical offset in px */
    double blur;      /* blur radius in px */
    double spread;    /* spread distance in px */
    char  *color;     /* any CSS color (named, hex, rgb(), hsl(), ...) */
} shadow_layer;

/*
 * Outcome of validating a color string. error[0] == '\0' when ok is true,
 * mirroring the TypeScript `{ ok: boolean; error: string | null }` shape.
 */
typedef struct color_result {
    int  ok;
    char error[128];
} color_result;

static color_result color_ok(void) {
    color_result r = { 1, "" };
    return r;
}

static color_result color_err(const char *msg) {
    color_result r = { 0, "" };
    snprintf(r.error, sizeof r.error, "%s", msg);
    return r;
}

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

static int is_named_color(const char *c) {
    size_t n = sizeof NAMED_COLORS / sizeof NAMED_COLORS[0];
    for (size_t i = 0; i < n; i++) {
        if (strcmp(c, NAMED_COLORS[i]) == 0) return 1;
    }
    return 0;
}

/* 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. */
static int is_hex_color(const char *s) {
    size_t n = strlen(s);
    if (n < 2 || s[0] != '#') return 0;
    size_t digits = n - 1;
    if (digits != 3 && digits != 6 && digits != 8) return 0;
    for (size_t i = 1; i < n; i++) {
        char c = s[i];
        if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) return 0;
    }
    return 1;
}

/* 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. */
static int is_function_color(const char *s, const char *const *prefixes, size_t count) {
    for (size_t i = 0; i < count; i++) {
        size_t plen = strlen(prefixes[i]);
        if (strncmp(s, prefixes[i], plen) != 0) continue;
        const char *rest = s + plen; /* begins at '(' */
        size_t rlen = strlen(rest);
        /* 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 (rlen >= 3 && rest[0] == '(' && rest[rlen - 1] == ')') {
            int bad = 0;
            for (size_t j = 1; j < rlen - 1 && !bad; j++) {
                if (rest[j] == ')') bad = 1;
            }
            if (!bad) return 1;
        }
    }
    return 0;
}

/* Copy `src` trimmed of ASCII whitespace and lower-cased into `dst`.
 * Python's parse_color does `.strip().lower()` once up front; this is that
 * canonicalizing copy. */
static void lower_trim_copy(char *dst, size_t cap, const char *src) {
    if (cap == 0) return;
    size_t len = strlen(src);
    while (len > 0 && isspace((unsigned char)src[len - 1])) len--;
    size_t start = 0;
    while (start < len && isspace((unsigned char)src[start])) start++;
    size_t j = 0;
    for (size_t i = start; i < len && j + 1 < cap; i++) {
        dst[j++] = (char)tolower((unsigned char)src[i]);
    }
    dst[j] = '\0';
}

/* Copy `src` trimmed of ASCII whitespace (original case preserved) into
 * `dst` — the `color.strip()` half of _normalize_color. */
static void trim_copy(char *dst, size_t cap, const char *src) {
    if (cap == 0) return;
    size_t len = strlen(src);
    while (len > 0 && isspace((unsigned char)src[len - 1])) len--;
    size_t start = 0;
    while (start < len && isspace((unsigned char)src[start])) start++;
    size_t n = len - start;
    if (n >= cap) n = cap - 1;
    memcpy(dst, src + start, n);
    dst[n] = '\0';
}

/* 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. Input is trimmed and lower-cased before matching,
 * exactly like the TS original. */
color_result parse_color(const char *color) {
    char c[256];
    lower_trim_copy(c, sizeof c, color ? color : "");
    if (c[0] == '\0') return color_err("empty color");
    if (is_named_color(c)) return color_ok();
    if (is_hex_color(c)) return color_ok();
    static const char *const RGB_PREFIXES[] = { "rgba", "rgb" };
    static const char *const HSL_PREFIXES[] = { "hsla", "hsl" };
    if (is_function_color(c, RGB_PREFIXES, 2) || is_function_color(c, HSL_PREFIXES, 2)) {
        return color_ok();
    }
    char msg[256];
    snprintf(msg, sizeof msg, "invalid color: %s", color ? color : "");
    return color_err(msg);
}

/* Keep a color when it parses and otherwise substitute a neutral translucent
 * black. This is what makes build_box_shadow total over arbitrary input. */
static void normalize_color(const char *color, char *out, size_t cap) {
    if (parse_color(color).ok) {
        trim_copy(out, cap, color);
    } else {
        snprintf(out, cap, "rgba(0,0,0,0.5)");
    }
}

/* Render a double the way JavaScript's template literals would: whole
 * numbers drop the decimal point (5, not 5.0). "%g" keeps the short form
 * for fractional values (5.5 -> "5.5"). */
static void format_number(double v, char *out, size_t cap) {
    if (v == floor(v) && fabs(v) < 1e15) {
        snprintf(out, cap, "%lld", (long long)v);
    } else {
        snprintf(out, cap, "%g", v);
    }
}

/* 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)". */
void format_layer(const shadow_layer *layer, char *out, size_t cap) {
    char x[32], y[32], blur[32], spread[32], color[256];
    format_number(layer->offset_x, x, sizeof x);
    format_number(layer->offset_y, y, sizeof y);
    format_number(layer->blur, blur, sizeof blur);
    format_number(layer->spread, spread, sizeof spread);
    normalize_color(layer->color, color, sizeof color);
    snprintf(out, cap, "%s%spx %spx %spx %spx %s",
             layer->inset ? "inset " : "", x, y, blur, spread, color);
}

/* Compose a full CSS box-shadow declaration from an ordered array of layers
 * (the first layer renders on top). count == 0 yields the CSS keyword
 * "none", matching the property's default value. */
void build_box_shadow(const shadow_layer *layers, size_t count, char *out, size_t cap) {
    if (cap == 0) return;
    if (count == 0) {
        snprintf(out, cap, "none");
        return;
    }
    size_t used = 0;
    for (size_t i = 0; i < count; i++) {
        char frag[320];
        format_layer(&layers[i], frag, sizeof frag);
        const char *sep = i == 0 ? "" : ", ";
        int n = snprintf(out + used, cap - used, "%s%s", sep, frag);
        if (n < 0) break;                                /* encoding error */
        used += (size_t)n;
        if (used >= cap) { used = cap - 1; break; }      /* truncated */
    }
}

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 →