Skip to content

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 →