CSS Gradient Generator — Go source
Build linear, radial, and conic CSS gradients with multiple color stops and positions. Live preview and copy-ready CSS.
This is the Go implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Package cssgradient is the Go twin of CosmoDev's src/lib/cssGradient.ts
// (dual source: the web lib is TypeScript, the CLI lib is Go — kept in
// lock-step). It builds linear/radial/conic CSS-gradient strings. Pure +
// deterministic, never panics. The table-driven tests in
// css-gradient-generator_test.go share vectors with src/lib/cssGradient.test.ts
// so the two implementations are held to the same contract.
//
// The algorithm mirrors the TS lib exactly: parse/validate each stop color,
// stable-sort stops by position ascending, pad to a black→white pair when
// fewer than two stops remain, round each position, then format per gradient
// type. Invalid stop colors fall back to #000000 exactly like normalizeColor
// in the TS lib.
package cssgradient
import (
"math"
"regexp"
"sort"
"strconv"
"strings"
)
// GradientType is the kind of CSS gradient to build. Mirrors the
// 'linear' | 'radial' | 'conic' literal union in the TS lib.
type GradientType string
const (
Linear GradientType = "linear"
Radial GradientType = "radial"
Conic GradientType = "conic"
)
// RadialShape is the ending shape of a radial gradient. Mirrors the
// 'circle' | 'ellipse' optional field in the TS lib. The zero value ""
// means "unset" and resolves to the TS default "circle".
type RadialShape string
const (
Circle RadialShape = "circle"
Ellipse RadialShape = "ellipse"
)
// GradientStop is a single color anchor in the gradient. Position is a
// percentage 0..100. Mirrors GradientStop in the TS lib.
type GradientStop struct {
Color string
Position float64
}
// GradientConfig fully describes the gradient to build. Mirrors
// GradientConfig in the TS lib. RadialShape only affects radial gradients;
// its zero value "" resolves to the TS default "circle".
type GradientConfig struct {
Type GradientType
Angle float64 // degrees (linear direction / conic start)
Stops []GradientStop
RadialShape RadialShape // optional; "" → "circle" (TS default)
}
// ColorResult is the outcome of ParseColor. Mirrors the
// { ok: boolean; error: string | null } return of parseColor in the TS lib.
type ColorResult struct {
OK bool
Error string
}
// Named colors accepted without a hex/function form. Compared after
// trim+lowercase. Mirrors NAMED_COLORS in the TS lib.
var namedColors = map[string]bool{
"transparent": true, "black": true, "white": true, "red": true,
"green": true, "blue": true, "yellow": true, "orange": true,
"purple": true, "pink": true, "gray": true, "grey": true,
"brown": true, "cyan": true, "magenta": true, "none": true,
"currentcolor": true,
}
var (
hex3or6 = regexp.MustCompile(`^#[0-9a-f]{3}([0-9a-f]{3})?$`)
hex8 = regexp.MustCompile(`^#[0-9a-f]{8}$`)
rgbaFn = regexp.MustCompile(`^rgba?\([^)]+\)$`)
hslaFn = regexp.MustCompile(`^hsla?\([^)]+\)$`)
)
// ParseColor reports whether a color string is a valid CSS color for gradient
// purposes: a known named color, a 3/6/8-digit hex, or an rgb()/rgba()/
// hsl()/hsla() function call. It is the Go twin of parseColor in the TS lib
// and must agree with it on every shared vector. The error string is non-empty
// iff OK is false (the TS lib's null maps to "").
func ParseColor(color string) ColorResult {
c := strings.ToLower(strings.TrimSpace(color))
if c == "" {
return ColorResult{OK: false, Error: "empty color"}
}
if namedColors[c] ||
hex3or6.MatchString(c) ||
hex8.MatchString(c) ||
rgbaFn.MatchString(c) ||
hslaFn.MatchString(c) {
return ColorResult{OK: true}
}
// Mirrors `invalid color: ${color}` — the original, untrimmed spelling.
return ColorResult{OK: false, Error: "invalid color: " + color}
}
// normalizeColor returns the trimmed color when it parses, else #000000 —
// the TS lib's fallback for an invalid stop color.
func normalizeColor(color string) string {
if ParseColor(color).OK {
return strings.TrimSpace(color)
}
return "#000000"
}
// formatNum formats a number the way a JS template literal would, so integer
// angles/positions render without a trailing ".0" (e.g. 90.0 → "90").
func formatNum(n float64) string {
return strconv.FormatFloat(n, 'f', -1, 64)
}
// BuildGradient renders a CSS gradient string. It is the Go twin of
// buildGradient in the TS lib and must agree with it on every shared vector.
// An unrecognized GradientType yields the empty string (the TS switch is
// exhaustive over the three literals and would return undefined otherwise).
func BuildGradient(config GradientConfig) string {
// Copy so we never mutate the caller's slice, then stable-sort by
// position ascending (V8's sort is stable; sort.SliceStable matches).
stops := make([]GradientStop, len(config.Stops))
copy(stops, config.Stops)
sort.SliceStable(stops, func(i, j int) bool {
return stops[i].Position < stops[j].Position
})
// Drop nonsensical entries; pad if fewer than 2.
if len(stops) < 2 {
stops = []GradientStop{
{Color: "#000000", Position: 0},
{Color: "#ffffff", Position: 100},
}
}
parts := make([]string, len(stops))
for i, s := range stops {
parts[i] = normalizeColor(s.Color) + " " + formatNum(math.Round(s.Position)) + "%"
}
stopsStr := strings.Join(parts, ", ")
switch config.Type {
case Linear:
return "linear-gradient(" + formatNum(config.Angle) + "deg, " + stopsStr + ")"
case Radial:
shape := config.RadialShape
if shape == "" {
shape = Circle // TS default via `config.radialShape ?? 'circle'`
}
return "radial-gradient(" + string(shape) + ", " + stopsStr + ")"
case Conic:
return "conic-gradient(from " + formatNum(config.Angle) + "deg, " + stopsStr + ")"
}
return "" // unrecognized type — safe zero value
}
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 →