Skip to content

CSS Gradient Generator — C source

Build linear, radial, and conic CSS gradients with multiple color stops and positions. Live preview and copy-ready CSS.

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

/* =============================================================================
 *  css-gradient-generator.c — CosmoDev polyglot showcase port of the
 *  `css-gradient-generator` tool
 *  -----------------------------------------------------------------------------
 *  Language : C (C11, standard library only)
 *  Source:   ported from src/lib/cssGradient.ts (the canonical, live TypeScript
 *             lib); mirrors src/tool-sources/css-gradient-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.
 *  -----------------------------------------------------------------------------
 *  Pure CSS-gradient builder. Build linear / radial / conic CSS gradient
 *  strings from a small config: a gradient type, an angle, and a list of color
 *  stops. Deterministic and side-effect free; invalid input degrades
 *  gracefully (unknown colors → solid black, too few stops → black/white
 *  default ramp) rather than failing. Returned strings are heap-allocated —
 *  the caller owns them and must free().
 * =============================================================================
 */

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

/* CSS gradient kinds we know how to render. */
typedef enum { GRADIENT_LINEAR, GRADIENT_RADIAL, GRADIENT_CONIC } gradient_type;

/* One color anchor on the gradient ramp. Position is a percentage 0..100. */
typedef struct {
    const char *color;
    double position;
} gradient_stop;

/* Full input to build_gradient. `radial_shape` is only meaningful for
 * GRADIENT_RADIAL; NULL falls back to "circle" (mirroring the TypeScript
 * `?? 'circle'` — an explicit "" passes through unchanged). */
typedef struct {
    gradient_type type;
    double angle;
    const gradient_stop *stops;
    size_t stop_count;        /* number of entries in `stops` */
    const char *radial_shape; /* NULL → "circle" */
} gradient_config;

/* Outcome of parse_color: an ok flag plus a human message (NULL when ok).
 * `error` is heap-allocated when set — free() it. */
typedef struct {
    int ok;
    char *error;
} color_result;

/* Named CSS colors this tool accepts. The full CSS spec defines ~148, but we
 * intentionally accept only the common, unambiguous set so output stays
 * predictable (mirrors the TypeScript allow-list). */
static const char *const NAMED_COLORS[] = {
    "transparent", "black", "white", "red", "green", "blue", "yellow", "orange",
    "purple", "pink", "gray", "grey", "brown", "cyan", "magenta", "none", "currentcolor",
};

/* Whitespace JS String.prototype.trim() strips. */
static const char *const WS = " \t\n\r\f\v";

/* --- tiny growable string buffer -------------------------------------------- */

typedef struct {
    char *data; /* NUL-terminated once non-NULL */
    size_t len;
    size_t cap;
} strbuf;

static int strbuf_grow(strbuf *b, size_t extra)
{
    if (b->data && b->len + extra + 1 <= b->cap) return 0;
    size_t cap = b->cap ? b->cap * 2 : 64;
    while (cap < b->len + extra + 1) cap *= 2;
    char *p = realloc(b->data, cap);
    if (!p) return -1;
    b->data = p;
    b->cap = cap;
    return 0;
}

static int strbuf_addn(strbuf *b, const char *s, size_t n)
{
    if (strbuf_grow(b, n)) return -1;
    memcpy(b->data + b->len, s, n);
    b->len += n;
    b->data[b->len] = '\0';
    return 0;
}

static int strbuf_add(strbuf *b, const char *s)
{
    return strbuf_addn(b, s, strlen(s));
}

/* printf-style append (vs*printf makes this C11-portable). */
static int strbuf_printf(strbuf *b, const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    int n = vsnprintf(NULL, 0, fmt, ap);
    va_end(ap);
    if (n < 0 || strbuf_grow(b, (size_t)n)) return -1;
    va_start(ap, fmt);
    vsnprintf(b->data + b->len, (size_t)n + 1, fmt, ap);
    va_end(ap);
    b->len += (size_t)n;
    return 0;
}

/* sprintf into a fresh heap string (a portable strdup/printf combo). */
static char *dupf(const char *fmt, ...)
{
    va_list ap;
    va_start(ap, fmt);
    int n = vsnprintf(NULL, 0, fmt, ap);
    va_end(ap);
    if (n < 0) return NULL;
    char *s = malloc((size_t)n + 1);
    if (!s) return NULL;
    va_start(ap, fmt);
    vsnprintf(s, (size_t)n + 1, fmt, ap);
    va_end(ap);
    return s;
}

/* --- color validation -------------------------------------------------------- */

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

/* True if a byte is a lowercase ASCII hex digit (`0-9` or `a-f`). Only the
 * lowercase form is accepted because parse_color lowercases its input before
 * testing, exactly like the TS regex `[0-9a-f]`. */
static int is_hex_byte(unsigned char b)
{
    return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f');
}

/* Validates a hex color by shape: `#` followed by exactly 3, 6, or 8 lowercase
 * hex digits. This collapses the two TS regexes (`#[0-9a-f]{3}([0-9a-f]{3})?`
 * and `#[0-9a-f]{8}`) into one structural check — C11 has no regex. */
static int is_hex_color(const char *c)
{
    size_t len = strlen(c);
    if (len != 4 && len != 7 && len != 9) return 0; /* '#' + {3,6,8} digits */
    if (c[0] != '#') return 0;
    for (size_t i = 1; i < len; i++)
        if (!is_hex_byte((unsigned char)c[i])) return 0;
    return 1;
}

/* Validates a functional color form `name(...)`: the string must start with one
 * of `openers` (e.g. "rgba(", "rgb("), end with ')', and have a nonempty body
 * containing no ')'. Mirrors the TS `^rgba?\([^)]+\)$` / `^hsla?\([^)]+\)$`.
 * Longer openers must come first so "rgba(" is tried before "rgb(". */
static int is_functional_color(const char *c, const char *const *openers, size_t opener_count)
{
    for (size_t i = 0; i < opener_count; i++) {
        size_t n = strlen(openers[i]);
        if (strncmp(c, openers[i], n) != 0) continue;
        size_t len = strlen(c);
        if (len < n + 1 || c[len - 1] != ')') return 0;
        size_t body_len = len - n - 1; /* body = c[n .. len-1) */
        if (body_len == 0) return 0;
        if (memchr(c + n, ')', body_len) != NULL) return 0;
        return 1;
    }
    return 0;
}

/* Length of `s` with leading/trailing whitespace removed; `*start_out`
 * receives the advanced pointer. Never modifies the input. */
static size_t trimmed_span(const char *s, const char **start_out)
{
    const char *start = s + strspn(s, WS);
    size_t len = strlen(start);
    while (len > 0 && strchr(WS, start[len - 1]) != NULL) len--;
    *start_out = start;
    return len;
}

/* Validate a CSS color string.
 *
 * Accepts named colors, #RGB / #RRGGBB / #RRGGBBAA hex, and rgb()/rgba()/
 * hsl()/hsla() functional forms. The input is trimmed and lowercased (into
 * scratch memory — the original is never modified) before testing.
 */
color_result parse_color(const char *color)
{
    color_result res = { 0, NULL };
    if (!color) color = "";

    const char *trimmed = NULL;
    size_t len = trimmed_span(color, &trimmed);

    char *c = malloc(len + 1);
    if (!c) return res; /* out of memory: report invalid — callers degrade to black */
    for (size_t i = 0; i < len; i++)
        c[i] = (char)tolower((unsigned char)trimmed[i]);
    c[len] = '\0';

    static const char *const RGB_OPENERS[] = { "rgba(", "rgb(" };
    static const char *const HSL_OPENERS[] = { "hsla(", "hsl(" };

    if (len == 0) {
        res.error = dupf("empty color");
    } else if (is_named_color(c)) {
        res.ok = 1;
    } else if (is_hex_color(c)) {
        res.ok = 1;
    } else if (is_functional_color(c, RGB_OPENERS, 2) || is_functional_color(c, HSL_OPENERS, 2)) {
        res.ok = 1;
    } else {
        res.error = dupf("invalid color: %s", color);
    }
    free(c);
    return res;
}

/* Coerce a possibly-invalid color to a safe value: valid → the trimmed
 * original (casing preserved), invalid → solid black. Heap-allocated; the
 * caller frees. Guarantees the gradient always has a usable color value. */
static char *normalize_color(const char *color)
{
    color_result r = parse_color(color);
    free(r.error);
    if (!r.ok) return dupf("%s", "#000000");

    const char *start = NULL;
    size_t len = trimmed_span(color, &start);
    char *out = malloc(len + 1);
    if (!out) return NULL; /* caller treats NULL as OOM */
    memcpy(out, start, len);
    out[len] = '\0';
    return out;
}

/* --- number formatting ------------------------------------------------------- */

/* Render a double the way JavaScript's template literal does — shortest
 * round-tripping decimal, no trailing ".0" on whole numbers. C has no direct
 * shortest formatting, so ask %g for increasing precision until the output
 * parses back to the same double. %g already drops trailing zeros, so 90.0
 * renders as "90", matching String(90). Heap-allocated; the caller frees. */
static char *format_number(double x)
{
    char buf[64];
    for (int prec = 1; prec <= 17; prec++) {
        snprintf(buf, sizeof buf, "%.*g", prec, x);
        if (strtod(buf, NULL) == x) break;
    }
    return dupf("%s", buf);
}

/* --- the builder ------------------------------------------------------------- */

/* Render a complete CSS gradient string (heap-allocated; caller frees).
 *
 * Stops are sorted ascending by position, stable: qsort() is not guaranteed
 * stable and qsort_r() is not portable C11, so an index array is
 * insertion-sorted instead — stable by construction (an index only moves past
 * strictly-smaller positions), matching modern JavaScript's Array.sort.
 * Fewer than two stops collapse to a black → white default ramp so the output
 * is always renderable. Positions round via floor(x + 0.5), which agrees with
 * Math.round on the non-negative 0..100 position domain.
 */
char *build_gradient(const gradient_config *config)
{
    size_t n = config->stop_count;

    /* Stable insertion sort over indices (see comment above). */
    size_t *order = NULL;
    if (n > 0) {
        order = malloc(n * sizeof *order);
        if (!order) return NULL;
        for (size_t i = 0; i < n; i++) order[i] = i;
        for (size_t i = 1; i < n; i++) {
            size_t key = order[i];
            double kp = config->stops[key].position;
            size_t j = i;
            while (j > 0 && config->stops[order[j - 1]].position > kp) {
                order[j] = order[j - 1];
                j--;
            }
            order[j] = key;
        }
    }

    const gradient_stop defaults[] = {
        { "#000000", 0.0 },
        { "#ffffff", 100.0 },
    };

    char *angle = format_number(config->angle);
    if (!angle) {
        free(order);
        return NULL;
    }

    strbuf out = { NULL, 0, 0 };

    switch (config->type) {
    case GRADIENT_LINEAR:
        if (strbuf_printf(&out, "linear-gradient(%sdeg, ", angle)) goto oom;
        break;
    case GRADIENT_RADIAL: {
        /* `?? 'circle'`: NULL → "circle"; non-NULL → verbatim (even if empty). */
        const char *shape = config->radial_shape ? config->radial_shape : "circle";
        if (strbuf_printf(&out, "radial-gradient(%s, ", shape)) goto oom;
        break;
    }
    case GRADIENT_CONIC:
        if (strbuf_printf(&out, "conic-gradient(from %sdeg, ", angle)) goto oom;
        break;
    default:
        /* Unknown type — mirrors the TypeScript, which falls off the switch and
         * implicitly returns undefined (an empty string here). */
        free(angle);
        free(order);
        return dupf("%s", "");
    }

    {
        const gradient_stop *ramp = config->stops;
        size_t count = n;
        const size_t *seq = order;
        if (n < 2) {
            ramp = defaults; /* already in position order */
            count = 2;
            seq = NULL;
        }

        for (size_t i = 0; i < count; i++) {
            if (i > 0 && strbuf_add(&out, ", ")) goto oom;
            const gradient_stop *s = &ramp[seq ? seq[i] : i];
            char *col = normalize_color(s->color);
            if (!col) goto oom;
            int failed = strbuf_printf(&out, "%s %ld%%", col, (long)floor(s->position + 0.5));
            free(col);
            if (failed) goto oom;
        }
    }

    if (strbuf_add(&out, ")")) goto oom;

    free(angle);
    free(order);
    return out.data ? out.data : dupf("%s", ""); /* empty stops still yield a head */

oom:
    free(angle);
    free(order);
    free(out.data);
    return NULL;
}

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 →