Skip to content

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 →