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 →