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 →