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, dependency-free,
// and never throws 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
// ShadowLayer record plus ParseColor, FormatLayer, and BuildBoxShadow.
//
// Language: C# 12 / .NET 8 (standard library only).
// Ported from src/lib/boxShadow.ts.
// Source: CosmoDev polyglot showcase port.
// License: display source — part of CosmoDev's polyglot tool pages.

using System;
using System.Collections.Generic;
using System.Text;
using System.Text.RegularExpressions;

namespace CosmoDev.BoxShadow;

/// <summary>A single layer in a CSS box-shadow stack.</summary>
public sealed record ShadowLayer(
    bool Inset,      // draw the shadow inside the box
    double OffsetX,  // horizontal offset in px
    double OffsetY,  // vertical offset in px
    double Blur,     // blur radius in px
    double Spread,   // spread distance in px
    string Color);   // any CSS color (named, hex, rgb(), hsl(), ...)

/// <summary>
/// Outcome of validating a color string. <see cref="Error"/> is
/// <see langword="null"/> when <see cref="Ok"/> is true, mirroring the
/// TypeScript <c>{ ok: boolean; error: string | null }</c> shape.
/// </summary>
public sealed record ColorResult(bool Ok, string? Error)
{
    internal static ColorResult OkValue() => new(true, null);

    internal static ColorResult ErrValue(string message) => new(false, message);
}

public static class BoxShadow
{
    // CSS named colors accepted without further inspection. ParseColor()
    // lower-cases its input first, so membership is effectively
    // case-insensitive.
    private static readonly HashSet<string> NamedColors = new(StringComparer.Ordinal)
    {
        "transparent", "black", "white", "red", "green", "blue", "yellow",
        "orange", "purple", "pink", "gray", "grey", "brown", "cyan", "magenta",
    };

    // Color-shape patterns. The input is already lower-cased + trimmed
    // before these run, so the hex classes use only [0-9a-f]. The contents
    // of rgb()/hsl() are not validated beyond a well-formed wrapper,
    // matching the live tool.
    private static readonly Regex HexShortRe = new(@"^#[0-9a-f]{3}([0-9a-f]{3})?$"); // #rgb or #rrggbb
    private static readonly Regex HexAlphaRe = new(@"^#[0-9a-f]{8}$");               // #rrggbbaa
    private static readonly Regex RgbRe = new(@"^rgba?\([^)]+\)$");                  // rgb() / rgba()
    private static readonly Regex HslRe = new(@"^hsla?\([^)]+\)$");                  // hsl() / hsla()

    /// <summary>
    /// 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.
    /// </summary>
    public static ColorResult ParseColor(string? color)
    {
        // Lower-case + trim once so every shape check below sees a canonical form.
        string c = (color ?? "").Trim().ToLowerInvariant();
        if (c.Length == 0) return ColorResult.ErrValue("empty color");
        if (NamedColors.Contains(c)) return ColorResult.OkValue();
        if (HexShortRe.IsMatch(c) || HexAlphaRe.IsMatch(c) || RgbRe.IsMatch(c) || HslRe.IsMatch(c))
        {
            return ColorResult.OkValue();
        }
        return ColorResult.ErrValue($"invalid color: {color}");
    }

    // Keep a color when it parses; otherwise substitute a neutral
    // translucent black. This is what makes BuildBoxShadow total over
    // arbitrary input.
    private static string NormalizeColor(string color) =>
        ParseColor(color).Ok ? color.Trim() : "rgba(0,0,0,0.5)";

    // Render a number with JavaScript parity: whole numbers drop the
    // trailing ".0" (so 5.0 -> "5", 5.5 -> "5.5"). "R" round-trip formatting
    // matches JS template-literal coercion for the fractional cases.
    private static string FormatNumber(double value) =>
        value == Math.Floor(value) && Math.Abs(value) < 1e15
            ? ((long)value).ToString()
            : value.ToString("R");

    /// <summary>
    /// 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)".
    /// </summary>
    public static string FormatLayer(ShadowLayer layer)
    {
        string prefix = layer.Inset ? "inset " : "";
        return $"{prefix}{FormatNumber(layer.OffsetX)}px "
            + $"{FormatNumber(layer.OffsetY)}px "
            + $"{FormatNumber(layer.Blur)}px "
            + $"{FormatNumber(layer.Spread)}px "
            + $"{NormalizeColor(layer.Color)}";
    }

    /// <summary>
    /// Compose a full CSS box-shadow declaration from an ordered list of
    /// layers (the first layer renders on top). An empty list yields the
    /// CSS keyword "none", matching the property's default value.
    /// </summary>
    public static string BuildBoxShadow(IReadOnlyList<ShadowLayer> layers)
    {
        if (layers.Count == 0) return "none";
        var sb = new StringBuilder();
        for (int i = 0; i < layers.Count; i++)
        {
            if (i > 0) sb.Append(", ");
            sb.Append(FormatLayer(layers[i]));
        }
        return sb.ToString();
    }
}

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 →