DEV Community

Dev Nestio
Dev Nestio

Posted on

I Built a Visual CSS Gradient Generator in Pure Vanilla JS — Linear, Radial & Conic, 150 Tests

I Built a Visual CSS Gradient Generator in Pure Vanilla JS — Linear, Radial & Conic, 150 Tests

CSS gradients are everywhere — hero sections, buttons, card backgrounds, loaders — yet writing them by hand is genuinely tedious. linear-gradient(135deg, #6c63ff 0%, #ff6584 100%) isn't hard to type, but tweaking angles, juggling multiple color stops, and switching between gradient types gets painful fast.

I built a zero-dependency browser tool that lets you compose gradients visually and copy the ready-to-paste CSS in one click.

Live tool → css-gradient-generator-ebf.pages.dev
All tools → devnestio.pages.dev


What the tool does

  • Three gradient types: Linear, Radial, and Conic — switchable with a tab
  • Color stops: add/remove stops freely; each has a color picker (<input type="color">), an alpha (opacity) slider, and a position (%) slider
  • Linear: angle 0–360° slider
  • Radial: shape (circle / ellipse), size (farthest-corner, closest-corner, farthest-side, closest-side), and position (center, top, top left, etc.)
  • Conic: from-angle 0–360° and position
  • Real-time preview that updates on every slider move
  • CSS output with both standard and -webkit- prefixed declarations
  • 12 presets: Sunset, Ocean, Forest, Purple Rain, Fire (radial), Arctic, Peach, Mint, Dusk, Candy (conic), Chrome, Aurora
  • Copy button — one click, clipboard ready

All in a single index.html. No npm install, no build step, no server.


Architecture: pure functions for testability

The tricky thing about testing UI tools is that the rendering logic is usually tangled with the DOM. My approach: extract every meaningful computation into a pure function that takes plain values and returns a string.

function hexToRgba(hex, alpha) {
  const clean = hex.replace('#', '');
  const full = clean.length === 3
    ? clean.split('').map(c => c + c).join('')
    : clean;
  const r = parseInt(full.slice(0, 2), 16);
  const g = parseInt(full.slice(2, 4), 16);
  const b = parseInt(full.slice(4, 6), 16);
  if (alpha >= 1) return `#${full.toLowerCase()}`;
  return `rgba(${r},${g},${b},${alpha.toFixed(2)})`;
}

function buildColorStop(stop) {
  return `${hexToRgba(stop.hex, stop.alpha)} ${stop.position}%`;
}

function buildLinearCSS(angle, stops) {
  return `linear-gradient(${angle}deg, ${stops.map(buildColorStop).join(', ')})`;
}

function buildRadialCSS(shape, size, position, stops) {
  return `radial-gradient(${shape} ${size} at ${position}, ${stops.map(buildColorStop).join(', ')})`;
}

function buildConicCSS(fromAngle, position, stops) {
  return `conic-gradient(from ${fromAngle}deg at ${position}, ${stops.map(buildColorStop).join(', ')})`;
}

function buildFullCSS(s) {
  const gradient = /* dispatch by type */;
  return `background: ${gradient};\nbackground: -webkit-${gradient};`;
}
Enter fullscreen mode Exit fullscreen mode

These functions have no DOM dependencies — they're pure input → output transformations. That means the test file is just Node.js with assert, no test framework required.


150 tests, zero dependencies

✅ 150 passed, ❌ 0 failed (150 total)
Enter fullscreen mode Exit fullscreen mode

The test file mirrors the pure functions and exercises them exhaustively:

// hexToRgba
test('hexToRgba: opaque 6-char returns hex', () => {
  assert.strictEqual(hexToRgba('#ff0000', 1), '#ff0000');
});
test('hexToRgba: 3-char shorthand expanded', () => {
  assert.strictEqual(hexToRgba('#f00', 1), '#ff0000');
});
test('hexToRgba: alpha 0.5 rgba correct', () => {
  assert.strictEqual(hexToRgba('#ff0000', 0.5), 'rgba(255,0,0,0.50)');
});

// buildLinearCSS
test('buildLinearCSS: angle 0', () => {
  const r = buildLinearCSS(0, [{ hex: '#ff0000', alpha: 1, position: 0 }, { hex: '#0000ff', alpha: 1, position: 100 }]);
  assert.ok(r.startsWith('linear-gradient(0deg'));
});

// buildRadialCSS
test('buildRadialCSS: position=top left', () => {
  const stops = [{ hex: '#fff', alpha: 1, position: 0 }, { hex: '#000', alpha: 1, position: 100 }];
  assert.ok(buildRadialCSS('ellipse', 'farthest-corner', 'top left', stops).includes('at top left'));
});

// buildConicCSS
test('buildConicCSS: from 180deg position bottom', () => {
  const stops = [{ hex: '#fff', alpha: 1, position: 0 }, { hex: '#000', alpha: 1, position: 100 }];
  assert.ok(buildConicCSS(180, 'bottom', stops).includes('from 180deg') && r.includes('at bottom'));
});
Enter fullscreen mode Exit fullscreen mode

The tests cover:

  • hexToRgba: 3-char expansion, uppercase normalisation, alpha → rgba conversion, channel parsing
  • buildColorStop: opaque and semi-transparent, all positions
  • buildLinearCSS / buildRadialCSS / buildConicCSS: all variants of type, shape, size, position, angle
  • buildFullCSS: webkit prefix present, two-line output, type dispatch
  • sortStops, validateHex, clamp, alphaToPercent, percentToAlpha: utility logic
  • Preset-style integration tests for Sunset, Ocean, Fire, Candy

The conic gradient type is underrated

Conic gradients get overlooked but they're perfect for pie charts, colour wheels, and clock-face effects. The CSS spec is:

background: conic-gradient(from 0deg at center, #f472b6 0%, #facc15 33%, #34d399 66%, #f472b6 100%);
Enter fullscreen mode Exit fullscreen mode

The tool supports all three types at the same fidelity — same stop system, same alpha control, same output format.


The -webkit- prefix question

I include both:

background: linear-gradient(135deg, #6c63ff 0%, #ff6584 100%);
background: -webkit-linear-gradient(135deg, #6c63ff 0%, #ff6584 100%);
Enter fullscreen mode Exit fullscreen mode

For modern browsers targeting 2024+, the -webkit- prefix for gradients is unnecessary — Chrome, Safari, Firefox, and Edge all handle unprefixed gradients fine. But a lot of CSS frameworks and legacy codebases still emit the prefix, so including it avoids confusion when pasting into an older project.


Running locally

# No install needed — just open the HTML
open src/index.html

# Tests
npm test
# ✅ 150 passed, ❌ 0 failed
Enter fullscreen mode Exit fullscreen mode

The tool

css-gradient-generator-ebf.pages.dev — free, no login, works offline after first load.

Part of devnestio.pages.dev — a growing collection of single-file browser tools for developers.

Source: single index.html + test/test.js, no build tooling, no framework.


What gradient effect do you use most often? Drop it in the comments — might make it into the presets.

Top comments (0)