Box-Shadow Generator — Python source
Design layered CSS box-shadows with offset, blur, spread, color, and inset. Live preview and copy-ready CSS.
This is the Python implementation — the same logic the interactive tool runs, in a shareable, citable form.
"""box-shadow-generator — Python 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 (standard
library only), and never raises 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 Python sibling of src/lib/boxShadow.ts (the canonical TypeScript
that powers the live tool). The public surface mirrors the TS: a ShadowLayer
dataclass plus parse_color, format_layer, and build_box_shadow.
Ported from src/lib/boxShadow.ts.
Display source — part of CosmoDev's polyglot tool pages.
"""
import re
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class ShadowLayer:
"""A single layer in a CSS box-shadow stack."""
inset: bool # draw the shadow inside the box
offset_x: float # horizontal offset in px
offset_y: float # vertical offset in px
blur: float # blur radius in px
spread: float # spread distance in px
color: str # any CSS color (named, hex, rgb(), hsl(), ...)
@dataclass
class ColorResult:
"""Outcome of validating a color string. ``error`` is None when ``ok``."""
ok: bool
error: Optional[str]
# CSS named colors accepted without further inspection. parse_color()
# lower-cases its input first, so membership is effectively case-insensitive.
_NAMED_COLORS = frozenset({
"transparent", "black", "white", "red", "green", "blue", "yellow",
"orange", "purple", "pink", "gray", "grey", "brown", "cyan", "magenta",
})
# Color-shape patterns. The input is already lower-cased + stripped 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.
_HEX_SHORT_RE = re.compile(r"^#[0-9a-f]{3}([0-9a-f]{3})?$") # #rgb or #rrggbb
_HEX_ALPHA_RE = re.compile(r"^#[0-9a-f]{8}$") # #rrggbbaa
_RGB_RE = re.compile(r"^rgba?\([^)]+\)$") # rgb() / rgba()
_HSL_RE = re.compile(r"^hsla?\([^)]+\)$") # hsl() / hsla()
def parse_color(color: str) -> ColorResult:
"""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.
"""
# Lower-case + strip once so every shape check below sees a canonical form.
c = (color or "").strip().lower()
if not c:
return ColorResult(ok=False, error="empty color")
if c in _NAMED_COLORS:
return ColorResult(ok=True, error=None)
if (
_HEX_SHORT_RE.match(c)
or _HEX_ALPHA_RE.match(c)
or _RGB_RE.match(c)
or _HSL_RE.match(c)
):
return ColorResult(ok=True, error=None)
return ColorResult(ok=False, error=f"invalid color: {color}")
def _normalize_color(color: str) -> str:
"""Keep a color when it parses; otherwise substitute a neutral translucent
black. This is what makes build_box_shadow total over arbitrary input."""
return color.strip() if parse_color(color).ok else "rgba(0,0,0,0.5)"
def _format_number(value: float) -> str:
"""Render a number with JavaScript parity: whole numbers drop the trailing
".0" (so 5.0 -> "5", 5.5 -> "5.5"). Mirrors JS template-literal coercion."""
if isinstance(value, int):
return str(value)
if value.is_integer():
return str(int(value))
return str(value)
def format_layer(layer: ShadowLayer) -> str:
"""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)"."""
prefix = "inset " if layer.inset else ""
return (
f"{prefix}{_format_number(layer.offset_x)}px "
f"{_format_number(layer.offset_y)}px "
f"{_format_number(layer.blur)}px "
f"{_format_number(layer.spread)}px "
f"{_normalize_color(layer.color)}"
)
def build_box_shadow(layers: List[ShadowLayer]) -> str:
"""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."""
if not layers:
return "none"
return ", ".join(format_layer(layer) for layer in layers)
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 →