Box-Shadow Generator — Kotlin source
Design layered CSS box-shadows with offset, blur, spread, color, and inset. Live preview and copy-ready CSS.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// box-shadow-generator — Kotlin 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,
// and never throws 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 Kotlin sibling of src/lib/boxShadow.ts (the canonical
// TypeScript that powers the live tool). The public surface mirrors the TS:
// a ShadowLayer data class plus parseColor, formatLayer, and buildBoxShadow.
//
// Language: Kotlin 1.9 (standard library only).
// Ported from src/lib/boxShadow.ts.
// Source: CosmoDev polyglot showcase port.
// License: display source — part of CosmoDev's polyglot tool pages.
package org.cosmolabs.cosmodev.boxshadow
/** A single layer in a CSS box-shadow stack. */
data class ShadowLayer(
val inset: Boolean, // draw the shadow inside the box
val offsetX: Double, // horizontal offset in px
val offsetY: Double, // vertical offset in px
val blur: Double, // blur radius in px
val spread: Double, // spread distance in px
val color: String, // any CSS color (named, hex, rgb(), hsl(), ...)
)
/**
* Outcome of validating a color string. [error] is null when [ok] is true,
* mirroring the TypeScript `{ ok: boolean; error: string | null }` shape.
*/
data class ColorResult(val ok: Boolean, val error: String?) {
companion object {
internal fun okValue() = ColorResult(ok = true, error = null)
internal fun errValue(message: String) = ColorResult(ok = false, error = message)
}
}
object BoxShadow {
// CSS named colors accepted without further inspection. parseColor()
// lower-cases its input first, so membership is effectively
// case-insensitive.
private val NAMED_COLORS = setOf(
"transparent", "black", "white", "red", "green", "blue", "yellow",
"orange", "purple", "pink", "gray", "grey", "brown", "cyan", "magenta",
)
// Color-shape patterns. The input is already lower-cased + trimmed
// 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.
private val HEX_SHORT_RE = Regex("""^#[0-9a-f]{3}([0-9a-f]{3})?$""") // #rgb or #rrggbb
private val HEX_ALPHA_RE = Regex("""^#[0-9a-f]{8}$""") // #rrggbbaa
private val RGB_RE = Regex("""^rgba?\([^)]+\)$""") // rgb() / rgba()
private val HSL_RE = Regex("""^hsla?\([^)]+\)$""") // hsl() / hsla()
/**
* 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.
*/
fun parseColor(color: String): ColorResult {
// Lower-case + trim once so every shape check below sees a canonical form.
val c = color.trim().lowercase()
if (c.isEmpty()) return ColorResult.errValue("empty color")
if (c in NAMED_COLORS) return ColorResult.okValue()
if (HEX_SHORT_RE.matches(c) || HEX_ALPHA_RE.matches(c) || RGB_RE.matches(c) || HSL_RE.matches(c)) {
return ColorResult.okValue()
}
return ColorResult.errValue("invalid color: $color")
}
// Keep a color when it parses; otherwise substitute a neutral
// translucent black. This is what makes buildBoxShadow total over
// arbitrary input.
private fun normalizeColor(color: String): String =
if (parseColor(color).ok) color.trim() else "rgba(0,0,0,0.5)"
// Render a number with JavaScript parity: whole numbers drop the
// trailing ".0" (so 5.0 -> "5", 5.5 -> "5.5"). Double.toString keeps
// the short form for fractions, mirroring JS template-literal coercion.
private fun formatNumber(value: Double): String =
if (value == Math.floor(value) && kotlin.math.abs(value) < 1e15) {
value.toLong().toString()
} else {
value.toString()
}
/**
* 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)".
*/
fun formatLayer(layer: ShadowLayer): String {
val prefix = if (layer.inset) "inset " else ""
return "$prefix${formatNumber(layer.offsetX)}px " +
"${formatNumber(layer.offsetY)}px " +
"${formatNumber(layer.blur)}px " +
"${formatNumber(layer.spread)}px " +
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.
*/
fun buildBoxShadow(layers: List<ShadowLayer>): String =
if (layers.isEmpty()) "none" else layers.joinToString(", ") { formatLayer(it) }
}
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 →