Skip to content

Box-Shadow Generator — Go source

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

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

// Package boxshadow is the Go twin of CosmoDev's src/lib/boxShadow.ts (dual
// source: the web lib is TypeScript, the CLI lib is Go — kept in lock-step).
// Pure + deterministic, never panics. The table-driven tests in
// box-shadow-generator_test.go share vectors with src/lib/boxShadow.test.ts so
// the two implementations are held to the same contract.
//
// It builds CSS box-shadow strings: validate a color, format one shadow layer,
// and join many layers. Invalid colors fall back to a translucent black
// (rgba(0,0,0,0.5)) rather than erroring — exactly like the TS lib.
package boxshadow

import (
	"regexp"
	"strconv"
	"strings"
)

// ShadowLayer is one box-shadow layer. It mirrors the ShadowLayer interface in
// src/lib/boxShadow.ts. The numeric fields are float64 to match TypeScript's
// number (fractional pixels like 0.5px are valid CSS and supported by the web
// tool); integers format without a trailing decimal.
type ShadowLayer struct {
	Inset   bool
	OffsetX float64
	OffsetY float64
	Blur    float64
	Spread  float64
	Color   string
}

// ColorResult is the outcome of ParseColor: OK reports validity, Error is the
// ""empty when OK. It mirrors { ok: boolean; error: string | null } in the TS lib.
type ColorResult struct {
	OK    bool
	Error string
}

// namedColors holds the CSS named colors ParseColor recognizes. Compared
// case-insensitively (input is lowercased before lookup). Mirrors NAMED_COLORS.
var namedColors = map[string]bool{
	"transparent": true, "black": true, "white": true, "red": true,
	"green": true, "blue": true, "yellow": true, "orange": true,
	"purple": true, "pink": true, "gray": true, "grey": true,
	"brown": true, "cyan": true, "magenta": true,
}

var (
	reHex3or6 = regexp.MustCompile(`^#[0-9a-f]{3}([0-9a-f]{3})?$`)
	reHex8    = regexp.MustCompile(`^#[0-9a-f]{8}$`)
	reRGBA    = regexp.MustCompile(`^rgba?\([^)]+\)$`)
	reHSLA    = regexp.MustCompile(`^hsla?\([^)]+\)$`)
)

// ParseColor validates a CSS color string. It accepts the named colors above,
// 3/6/8-digit hex, and rgb()/rgba()/hsl()/hsla() functional notation. It is the
// Go twin of parseColor() in src/lib/boxShadow.ts and never panics: empty input
// yields {OK:false, Error:"empty color"}, any other unparseable input yields
// {OK:false, Error:"invalid color: <original>"}.
func ParseColor(color string) ColorResult {
	c := strings.ToLower(strings.TrimSpace(color))
	if c == "" {
		return ColorResult{OK: false, Error: "empty color"}
	}
	if namedColors[c] {
		return ColorResult{OK: true}
	}
	if reHex3or6.MatchString(c) || reHex8.MatchString(c) {
		return ColorResult{OK: true}
	}
	if reRGBA.MatchString(c) || reHSLA.MatchString(c) {
		return ColorResult{OK: true}
	}
	return ColorResult{OK: false, Error: "invalid color: " + color}
}

// normalizeColor returns the trimmed color when it is valid, otherwise the
// fallback translucent black. Mirrors normalizeColor() in the TS lib.
func normalizeColor(color string) string {
	if ParseColor(color).OK {
		return strings.TrimSpace(color)
	}
	return "rgba(0,0,0,0.5)"
}

// formatNum renders a shadow length the way the TS template literal does:
// integers have no decimal point, fractions keep their minimal form
// (strconv FormatFloat 'f' -1 → "4", "-5", "1.5", "0").
func formatNum(n float64) string {
	return strconv.FormatFloat(n, 'f', -1, 64)
}

// FormatLayer renders one ShadowLayer as its CSS box-shadow fragment. It is the
// Go twin of formatLayer() in src/lib/boxShadow.ts. An invalid color falls back
// to rgba(0,0,0,0.5) rather than erroring, matching the TS lib.
func FormatLayer(layer ShadowLayer) string {
	var b strings.Builder
	if layer.Inset {
		b.WriteString("inset ")
	}
	b.WriteString(formatNum(layer.OffsetX))
	b.WriteString("px ")
	b.WriteString(formatNum(layer.OffsetY))
	b.WriteString("px ")
	b.WriteString(formatNum(layer.Blur))
	b.WriteString("px ")
	b.WriteString(formatNum(layer.Spread))
	b.WriteString("px ")
	b.WriteString(normalizeColor(layer.Color))
	return b.String()
}

// BuildBoxShadow joins one or more shadow layers into a full CSS box-shadow
// value. It is the Go twin of buildBoxShadow() in src/lib/boxShadow.ts: an empty
// slice returns "none", otherwise layers are formatted and joined with ", ".
func BuildBoxShadow(layers []ShadowLayer) string {
	if len(layers) == 0 {
		return "none"
	}
	parts := make([]string, len(layers))
	for i, l := range layers {
		parts[i] = FormatLayer(l)
	}
	return strings.Join(parts, ", ")
}

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 →