Box-Shadow Generator — Ruby source
Design layered CSS box-shadows with offset, blur, spread, color, and inset. Live preview and copy-ready CSS.
This is the Ruby implementation — the same logic the interactive tool runs, in a shareable, citable form.
# box-shadow-generator — Ruby 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 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 Ruby sibling of src/lib/boxShadow.ts (the canonical TypeScript
# that powers the live tool). The public surface mirrors the TS: a
# ShadowLayer Struct plus parse_color, format_layer, and build_box_shadow.
#
# Language: Ruby 3.2 (standard library only).
# Ported from src/lib/boxShadow.ts.
# Source: CosmoDev polyglot showcase port.
# License: display source — part of CosmoDev's polyglot tool pages.
# A single layer in a CSS box-shadow stack.
ShadowLayer = Struct.new(
:inset, # draw the shadow inside the box
:offset_x, # horizontal offset in px
:offset_y, # vertical offset in px
:blur, # blur radius in px
:spread, # spread distance in px
:color, # any CSS color (named, hex, rgb(), hsl(), ...)
keyword_init: true
)
# Outcome of validating a color string. `error` is nil when `ok` is true,
# mirroring the TypeScript `{ ok: boolean; error: string | null }` shape.
ColorResult = Struct.new(:ok, :error, keyword_init: true)
module BoxShadow
# CSS named colors accepted without further inspection. parse_color()
# lower-cases its input first, so membership is effectively
# case-insensitive.
NAMED_COLORS = %w[
transparent black white red green blue yellow
orange purple pink gray grey brown cyan magenta
].freeze
# 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. \A/\z anchor the whole string (Ruby's ^/$ match line ends).
HEX_SHORT_RE = /\A#[0-9a-f]{3}([0-9a-f]{3})?\z/ # #rgb or #rrggbb
HEX_ALPHA_RE = /\A#[0-9a-f]{8}\z/ # #rrggbbaa
RGB_RE = /\Argba?\([^)]+\)\z/ # rgb() / rgba()
HSL_RE = /\Ahsla?\([^)]+\)\z/ # hsl() / hsla()
module_function
# 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.
def parse_color(color)
# Lower-case + strip once so every shape check below sees a canonical form.
c = color.to_s.strip.downcase
return ColorResult.new(ok: false, error: 'empty color') if c.empty?
return ColorResult.new(ok: true, error: nil) if NAMED_COLORS.include?(c)
return ColorResult.new(ok: true, error: nil) if HEX_SHORT_RE.match?(c) || HEX_ALPHA_RE.match?(c) || RGB_RE.match?(c) || HSL_RE.match?(c)
ColorResult.new(ok: false, error: "invalid color: #{color}")
end
# Keep a color when it parses; otherwise substitute a neutral translucent
# black. This is what makes build_box_shadow total over arbitrary input.
def normalize_color(color)
parse_color(color).ok ? color.strip : 'rgba(0,0,0,0.5)'
end
# Render a number with JavaScript parity: whole numbers drop the trailing
# ".0" (so 5.0 -> "5", 5.5 -> "5.5"). Float#to_s keeps the short form for
# fractions, mirroring JS template-literal coercion.
def format_number(value)
return value.to_i.to_s if value == value.truncate && value.abs < 1e15
value.to_s
end
private_class_method :normalize_color, :format_number
# 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)".
def format_layer(layer)
prefix = layer.inset ? 'inset ' : ''
"#{prefix}#{format_number(layer.offset_x)}px " \
"#{format_number(layer.offset_y)}px " \
"#{format_number(layer.blur)}px " \
"#{format_number(layer.spread)}px " \
"#{normalize_color(layer.color)}"
end
# 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.
def build_box_shadow(layers)
return 'none' if layers.empty?
layers.map { |layer| format_layer(layer) }.join(', ')
end
end
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 →