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 →