Skip to content

Box-Shadow Generator — Rust source

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

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

// box-shadow-generator — Rust 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
// (Rust stdlib only — no `regex` crate), 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 Rust sibling of src/lib/boxShadow.ts (the canonical TypeScript
// that powers the live tool). The public surface mirrors the TS: a ShadowLayer
// struct plus parse_color, format_layer, and build_box_shadow. Because the
// stdlib ships no regex engine, the color-shape checks are written by hand —
// they accept exactly what the TS regexes do, nothing more.
//
// Ported from src/lib/boxShadow.ts.
// Display source — part of CosmoDev's polyglot tool pages.

/// A single layer in a CSS box-shadow stack.
#[derive(Debug, Clone, PartialEq)]
pub struct ShadowLayer {
    pub inset: bool,    // draw the shadow inside the box
    pub offset_x: f64,  // horizontal offset in px
    pub offset_y: f64,  // vertical offset in px
    pub blur: f64,      // blur radius in px
    pub spread: f64,    // spread distance in px
    pub color: String,  // any CSS color (named, hex, rgb(), hsl(), ...)
}

/// Outcome of validating a color string. `error` is `None` when `ok` is true,
/// mirroring the TypeScript `{ ok: boolean; error: string | null }` shape.
#[derive(Debug, Clone, PartialEq)]
pub struct ColorResult {
    pub ok: bool,
    pub error: Option<String>,
}

impl ColorResult {
    fn ok_value() -> Self {
        ColorResult { ok: true, error: None }
    }
    fn err_value(msg: String) -> Self {
        ColorResult { ok: false, error: Some(msg) }
    }
}

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

/// 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 is_hex_color(s: &str) -> bool {
    let chars: Vec<char> = s.chars().collect();
    if chars.len() < 2 || chars[0] != '#' {
        return false;
    }
    let n = chars.len() - 1;
    if n != 3 && n != 6 && n != 8 {
        return false;
    }
    chars[1..]
        .iter()
        .all(|&c| c.is_ascii_digit() || ('a'..='f').contains(&c))
}

/// 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 is_function_color(s: &str, prefixes: &[&str]) -> bool {
    for &prefix in prefixes {
        if let Some(rest) = s.strip_prefix(prefix) {
            let chars: Vec<char> = rest.chars().collect();
            // 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 chars.len() >= 3 && chars[0] == '(' && chars[chars.len() - 1] == ')' {
                let inside: String = chars[1..chars.len() - 1].iter().collect();
                if !inside.is_empty() && !inside.contains(')') {
                    return true;
                }
            }
        }
    }
    false
}

/// Validate a CSS color string. Accepts the curated named-color set plus hex
/// (#rgb, #rrggbb, #rrggbbaa), rgb()/rgba(), and hsl()/hsla() forms. Input is
/// trimmed and lower-cased before matching, exactly like the TS original.
pub fn parse_color(color: &str) -> ColorResult {
    let c = color.trim().to_lowercase();
    if c.is_empty() {
        return ColorResult::err_value("empty color".to_string());
    }
    if NAMED_COLORS.contains(&c.as_str()) {
        return ColorResult::ok_value();
    }
    if is_hex_color(&c) {
        return ColorResult::ok_value();
    }
    if is_function_color(&c, &["rgba", "rgb"]) || is_function_color(&c, &["hsla", "hsl"]) {
        return ColorResult::ok_value();
    }
    ColorResult::err_value(format!("invalid color: {}", color))
}

/// Keep a color when it parses and otherwise substitute a neutral translucent
/// black. This is what makes `build_box_shadow` total over arbitrary input.
fn normalize_color(color: &str) -> String {
    if parse_color(color).ok {
        color.trim().to_string()
    } else {
        "rgba(0,0,0,0.5)".to_string()
    }
}

/// Render an f64 the way JavaScript's template literals would: whole numbers
/// drop the decimal point (5, not 5.0). Rust's default `{}` Display for floats
/// already does this, so we lean on it.
fn format_number(v: f64) -> String {
    format!("{}", 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)".
pub fn format_layer(layer: &ShadowLayer) -> String {
    format!(
        "{}{}px {}px {}px {}px {}",
        if layer.inset { "inset " } else { "" },
        format_number(layer.offset_x),
        format_number(layer.offset_y),
        format_number(layer.blur),
        format_number(layer.spread),
        normalize_color(&layer.color),
    )
}

/// 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".
pub fn build_box_shadow(layers: &[ShadowLayer]) -> String {
    if layers.is_empty() {
        return "none".to_string();
    }
    layers
        .iter()
        .map(format_layer)
        .collect::<Vec<_>>()
        .join(", ")
}

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 →