Skip to content

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 →