Skip to content

Box-Shadow Generator — PHP source

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

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

<?php
/**
 * box-shadow-generator — PHP 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
 * (PHP standard library only), and never throws: invalid colors quietly fall
 * back to a neutral translucent black, so a single bad color never breaks the
 * whole stack.
 *
 * This is the PHP sibling of src/lib/boxShadow.ts (the canonical TypeScript
 * that powers the live tool). The public surface mirrors the TS: a ShadowLayer
 * class plus parseColor, formatLayer, and buildBoxShadow.
 *
 * Ported from src/lib/boxShadow.ts.
 * Display source — part of CosmoDev's polyglot tool pages.
 */

declare(strict_types=1);

namespace CosmoDev\BoxShadow;

/**
 * A single layer in a CSS box-shadow stack. Readonly, constructor-promoted
 * properties mirror the TypeScript ShadowLayer shape.
 */
class ShadowLayer
{
    public function __construct(
        public bool   $inset,
        public float  $offsetX,
        public float  $offsetY,
        public float  $blur,
        public float  $spread,
        public string $color,
    ) {}
}

/**
 * CSS named colors accepted without further inspection. Stored as a lookup
 * map so the membership check is an O(1) isset. parseColor() lower-cases its
 * input first, so the comparison is effectively case-insensitive.
 */
function isNamedColor(string $c): bool
{
    static $named = [
        'transparent' => 1, 'black' => 1, 'white' => 1, 'red' => 1,
        'green' => 1, 'blue' => 1, 'yellow' => 1, 'orange' => 1,
        'purple' => 1, 'pink' => 1, 'gray' => 1, 'grey' => 1,
        'brown' => 1, 'cyan' => 1, 'magenta' => 1,
    ];
    return isset($named[$c]);
}

/**
 * Validate a CSS color string.
 *
 * Accepts the named colors above plus hex (#rgb, #rrggbb, #rrggbbaa), rgb()/
 * rgba(), and hsl()/hsla() forms. The functional notations are checked for
 * well-formed wrappers only — their contents are otherwise unchecked, matching
 * the live tool's deliberately permissive behavior.
 *
 * @return array{ok: bool, error: string|null}
 */
function parseColor(string $color): array
{
    // Lower-case + trim once so every shape check below sees a canonical form.
    $c = strtolower(trim($color));
    if ($c === '') {
        return ['ok' => false, 'error' => 'empty color'];
    }
    if (isNamedColor($c)) {
        return ['ok' => true, 'error' => null];
    }
    // Input is already lower-cased, so the hex classes use [0-9a-f] only.
    // preg_match returns 1 on a match (0 on no match, false on error).
    if (preg_match('/^#[0-9a-f]{3}([0-9a-f]{3})?$/', $c) === 1) {
        return ['ok' => true, 'error' => null];
    }
    if (preg_match('/^#[0-9a-f]{8}$/', $c) === 1) {
        return ['ok' => true, 'error' => null];
    }
    if (preg_match('/^rgba?\([^)]+\)$/', $c) === 1) {
        return ['ok' => true, 'error' => null];
    }
    if (preg_match('/^hsla?\([^)]+\)$/', $c) === 1) {
        return ['ok' => true, 'error' => null];
    }
    return ['ok' => false, 'error' => "invalid color: {$color}"];
}

/**
 * Coerce a color to a usable value: keep it when it parses, otherwise
 * substitute a neutral translucent black. Keeping this total is what lets
 * buildBoxShadow never produce a broken declaration.
 */
function normalizeColor(string $color): string
{
    return parseColor($color)['ok'] ? trim($color) : 'rgba(0,0,0,0.5)';
}

/**
 * Render a number with JavaScript parity: whole numbers have no decimal point
 * (5.0 -> "5"). PHP's default float-to-string coercion already does this, so we
 * lean on it rather than reimplementing rounding.
 */
function formatNumber(float $n): string
{
    return (string) $n;
}

/**
 * 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)".
 */
function formatLayer(ShadowLayer $layer): string
{
    $inset = $layer->inset ? 'inset ' : '';
    return sprintf(
        '%s%spx %spx %spx %spx %s',
        $inset,
        formatNumber($layer->offsetX),
        formatNumber($layer->offsetY),
        formatNumber($layer->blur),
        formatNumber($layer->spread),
        normalizeColor($layer->color),
    );
}

/**
 * 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.
 *
 * @param ShadowLayer[] $layers
 */
function buildBoxShadow(array $layers): string
{
    if (count($layers) === 0) {
        return 'none';
    }
    return implode(', ', array_map(fn(ShadowLayer $l) => formatLayer($l), $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 →