Skip to content

CSS Gradient Generator — Java source

Build linear, radial, and conic CSS gradients with multiple color stops and positions. Live preview and copy-ready CSS.

This is the Java implementation — the same logic the interactive tool runs, in a shareable, citable form.

// =============================================================================
//  css-gradient-generator.java — CosmoDev polyglot showcase port of the
//  `css-gradient-generator` tool
//  -----------------------------------------------------------------------------
//  Language : Java (Java 17, standard library only)
//  Source:   ported from src/lib/cssGradient.ts (the canonical, live TypeScript
//             lib); mirrors src/tool-sources/css-gradient-generator/{python.py,rust.rs}
//  License  : display source — part of CosmoDev's polyglot tool pages
//             (dev.cosmolabs.org). Shown verbatim alongside the JS/TS/Go/Rust/
//             Python ports and the other language ports.
//  -----------------------------------------------------------------------------
//  Pure CSS-gradient builder. Build linear / radial / conic CSS gradient
//  strings from a small config record. Deterministic and side-effect free;
//  invalid input degrades gracefully (unknown colors → solid black, too few
//  stops → black/white default ramp) rather than throwing.
// =============================================================================

package com.cosmolabs.cosmodev.cssgradient;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.Set;

/** Pure CSS-gradient builder — Java polyglot showcase port. */
public final class CssGradientGenerator {

    private CssGradientGenerator() {}

    /** CSS gradient kinds we know how to render. */
    public enum GradientType { LINEAR, RADIAL, CONIC }

    /** One color anchor on the gradient ramp. Position is a percentage 0..100. */
    public record GradientStop(String color, double position) {}

    /**
     * Full input to {@link #buildGradient}. {@code radialShape} is only
     * meaningful for {@link GradientType#RADIAL}; {@code null} falls back to
     * "circle" (mirroring the TypeScript {@code ?? 'circle'} — an explicit
     * {@code ""} passes through unchanged).
     */
    public record GradientConfig(GradientType type, double angle,
                                 List<GradientStop> stops, String radialShape) {}

    /** Outcome of {@link #parseColor}: an ok flag plus a human message (null when ok). */
    public record ColorResult(boolean ok, String error) {
        public static ColorResult valid() { return new ColorResult(true, null); }
        public static ColorResult invalid(String message) { return new ColorResult(false, message); }
    }

    /**
     * Named CSS colors this tool accepts. The full CSS spec defines ~148, but
     * we intentionally accept only the common, unambiguous set so output stays
     * predictable (mirrors the TypeScript allow-list).
     */
    private static final Set<String> NAMED_COLORS = Set.of(
        "transparent", "black", "white", "red", "green", "blue", "yellow", "orange",
        "purple", "pink", "gray", "grey", "brown", "cyan", "magenta", "none", "currentcolor");

    /** True if a char is a lowercase ASCII hex digit ('0'-'9' or 'a'-'f'). Only
     * the lowercase form is accepted because {@link #parseColor} lowercases its
     * input before testing, exactly like the TS regex {@code [0-9a-f]}. */
    private static boolean isHexByte(char ch) {
        return (ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f');
    }

    /** Validates a hex color by shape: '#' followed by exactly 3, 6, or 8
     * lowercase hex digits. This collapses the two TS regexes
     * ({@code #[0-9a-f]{3}([0-9a-f]{3})?} and {@code #[0-9a-f]{8}}) into one
     * structural check — no regex machinery needed. */
    private static boolean isHexColor(String c) {
        int n = c.length();
        if (n != 4 && n != 7 && n != 9) return false; // '#' + {3,6,8} digits
        if (c.charAt(0) != '#') return false;
        for (int i = 1; i < n; i++) {
            if (!isHexByte(c.charAt(i))) return false;
        }
        return true;
    }

    /** Validates a functional color form "name(...)": the string must start
     * with one of {@code openers} (e.g. "rgba(", "rgb("), end with ')', and
     * have a nonempty body containing no ')'. Mirrors the TS
     * {@code ^rgba?\([^)]+\)$} / {@code ^hsla?\([^)]+\)$}. Longer openers must
     * come first so "rgba(" is tried before "rgb(". */
    private static boolean isFunctionalColor(String c, List<String> openers) {
        for (String opener : openers) {
            if (c.startsWith(opener)) {
                if (!c.endsWith(")")) return false;
                String body = c.substring(opener.length(), c.length() - 1);
                return !body.isEmpty() && !body.contains(")");
            }
        }
        return false;
    }

    /** Validate a CSS color string.
     *
     * Accepts named colors, #RGB / #RRGGBB / #RRGGBBAA hex, and rgb()/rgba()/
     * hsl()/hsla() functional forms. The input is trimmed and lowercased
     * (ROOT locale — color syntax is ASCII) before testing. */
    public static ColorResult parseColor(String color) {
        String c = (color == null ? "" : color).strip().toLowerCase(Locale.ROOT);
        if (c.isEmpty()) return ColorResult.invalid("empty color");
        if (NAMED_COLORS.contains(c)) return ColorResult.valid();
        if (isHexColor(c)) return ColorResult.valid();
        if (isFunctionalColor(c, List.of("rgba(", "rgb("))
                || isFunctionalColor(c, List.of("hsla(", "hsl("))) {
            return ColorResult.valid();
        }
        return ColorResult.invalid("invalid color: " + color);
    }

    /** Coerce a possibly-invalid color to a safe value: valid → the trimmed
     * original (casing preserved), invalid → solid black. Guarantees the
     * gradient always has a usable color value. */
    private static String normalizeColor(String color) {
        return parseColor(color).ok() ? color.strip() : "#000000";
    }

    /** Render a double the way JavaScript's template literal does.
     *
     * Double.toString keeps enough digits to round-trip, and prints whole
     * doubles with a trailing ".0"; we strip it so 90.0 renders as "90",
     * matching String(90). (JDK 19+ tightened Double.toString to the strictly
     * shortest form; for this tool's plain-decimal angle domain the two agree.) */
    private static String formatNumber(double x) {
        String s = Double.toString(x);
        return s.endsWith(".0") ? s.substring(0, s.length() - 2) : s;
    }

    /** Render a complete CSS gradient string.
     *
     * Stops are sorted ascending by position (stable — List.sort uses TimSort,
     * matching modern JavaScript's Array.sort; a NaN position — out of domain —
     * sorts last per Double.compare). Fewer than two stops collapse to a
     * black → white default ramp so the output is always renderable. Positions
     * round via floor(x + 0.5), which agrees with Math.round on the
     * non-negative 0..100 position domain. */
    public static String buildGradient(GradientConfig config) {
        List<GradientStop> stops = new ArrayList<>(config.stops());
        stops.sort(Comparator.comparingDouble(GradientStop::position));

        if (stops.size() < 2) {
            stops = new ArrayList<>(List.of(
                new GradientStop("#000000", 0.0),
                new GradientStop("#ffffff", 100.0)));
        }

        StringBuilder stopsStr = new StringBuilder();
        for (int i = 0; i < stops.size(); i++) {
            GradientStop s = stops.get(i);
            if (i > 0) stopsStr.append(", ");
            long rounded = (long) Math.floor(s.position() + 0.5);
            stopsStr.append(normalizeColor(s.color())).append(' ').append(rounded).append('%');
        }

        String angle = formatNumber(config.angle());

        switch (config.type()) {
            case LINEAR:
                return "linear-gradient(" + angle + "deg, " + stopsStr + ")";
            case RADIAL: {
                // `?? 'circle'`: null → "circle"; non-null → verbatim (even if empty).
                String shape = config.radialShape() != null ? config.radialShape() : "circle";
                return "radial-gradient(" + shape + ", " + stopsStr + ")";
            }
            case CONIC:
                return "conic-gradient(from " + angle + "deg, " + stopsStr + ")";
            default:
                return ""; // unreachable for the enum — mirrors the TS fall-through
        }
    }
}

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 →