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};`;
}
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)
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'));
});
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%);
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%);
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
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)