GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

CSS Generator: 9 Sliders That Write the Code

A CSS generator is a small panel that turns a CSS value into a control you can move: drag a slider, read the code.

Auto-plays · click a tile to jump to its section · all nine in one zip

The order is the order in which a screen gets dressed. A shadow lifts the card off the page (01), a gradient fills the ground behind it (02), the corners soften (03), a photo is cut to shape (04), the color is graded (05), the cells spread across the width (06), the headline takes an outline (07), the type sizes lock into a ladder (08), and the motion is timed last (09). The second of the nine panels is a css gradient generator, and the wiring it uses — slider to custom property to code box — is identical in all of them, which is why no panel can show one value and print another. Next door in the same creative pillar sit CSS Background Pattern Generator: 9 Seeded Scenes, where a seed rather than a hand decides the numbers, and JavaScript Physics Simulation: 9 Demos, No Library, where the value you drag is a force.

01Stacked box shadow

Four sliders hold the x, y, blur, and spread axes while two copies of the same shadow stack up — the panel you open to decide how far a card or a button floats off the page.

Stacked box shadow
Open in a new tab

01 · Stacked box shadow — Measured area 10.931% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

box-shadow@property--shadow-y

Four numbers kept in one place

// Register the four axes as integers — registration is what lets keyframes interpolate them and counter() read them
@property --shadow-x { syntax: "<integer>"; inherits: true; initial-value: 0; }
@property --shadow-y { syntax: "<integer>"; inherits: true; initial-value: 10; }
@property --shadow-blur { syntax: "<integer>"; inherits: true; initial-value: 20; }
@property --shadow-spread { syntax: "<integer>"; inherits: true; initial-value: -4; }

// The digits in the code box read the same properties through a counter — screen and code see one value
.gen__v--x::after { counter-reset: v var(--shadow-x); content: counter(v); }

Because the numbers live only in those four registered properties, the card's box-shadow, the slider thumb position, and the digits in the code box cannot disagree, and the grid preview animates those same properties through keyframes for 10.93% cumulative moved area, 110.3 average intensity, and 23 of 23 frames with motion.

Two layers that make the depth

box-shadow:
  calc(var(--shadow-x) * 1px) calc(var(--shadow-y) * 1px) calc(var(--shadow-blur) * 1px) calc(var(--shadow-spread) * 1px) rgba(23, 20, 26, .34),
  calc(var(--shadow-x) * .5px) calc(var(--shadow-y) * .5px) calc(var(--shadow-blur) * .5px) 0 rgba(23, 20, 26, .22);

The lower layer is the same shadow at half the distance and half the blur, so the edge stays clean as the values grow, and that is what makes the pair usable as the base of a design system's shadow steps.

02Angle and stop gradient

An angle slider rotates the direction of the linear-gradient while two stop sliders push the point where the colors meet — the panel to reach for when a hero background or a button fill has to be settled by eye.

Angle and stop gradient
Open in a new tab

02 · Angle and stop gradient — Measured area 8.818% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

linear-gradient--grad-angle--grad-stop-b

One angle decides the direction

// The angle is a single integer; calc attaches the degree unit and the two stops push the color boundary
.gen__preview {
  background: linear-gradient(
    calc(var(--grad-angle) * 1deg),
    $subject-blue calc(var(--grad-stop-a) * 1%),
    $subject-pink calc(var(--grad-stop-b) * 1%)
  );
}

Turning from 45 degrees to 315 degrees repaints the entire box rather than an edge of it, so the clip comes back with 8.82% cumulative moved area, 70.1 average intensity, and 23 of 23 frames with motion.

Thumbs computed from the same values

.gen__row:nth-child(1) .gen__thumb { --pct: calc(var(--grad-angle) / 360 * 100); }
.gen__row:nth-child(2) .gen__thumb { --pct: var(--grad-stop-a); }
.gen__row:nth-child(3) .gen__thumb { --pct: var(--grad-stop-b); }

The thumb positions are worked out from the same three properties, so pushing stop A toward 0 and stop B toward 100 blends the two colors across the whole box while sliding them together leaves something close to a hard line.

03Eight-handle corner radius

Four corner sliders and one shared vertical radius write the slash syntax between them, which is how a blob that no single border-radius number can produce gets built by hand.

Eight-handle corner radius
Open in a new tab

03 · Eight-handle corner radius — Measured area 5.821% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

border-radius--r-tl--r-vy

Per-corner longhand equals the slash syntax

// The per-corner longhand takes two values (horizontal vertical) and is equivalent to the slash syntax — with the
// value attached to its own property, it is also easier to read which corner is moving
border-top-left-radius: calc(var(--r-tl) * 1px) calc(var(--r-vy) * 1px);
border-top-right-radius: calc(var(--r-tr) * 1px) calc(var(--r-vy) * 1px);
border-bottom-right-radius: calc(var(--r-br) * 1px) calc(var(--r-vy) * 1px);
border-bottom-left-radius: calc(var(--r-bl) * 1px) calc(var(--r-vy) * 1px);

Making the top-right and bottom-left large while the other two stay small tilts the shape like a water drop, and the code box prints the short form border-radius: 12px 48px 12px 48px / 28px while the preview keeps the longhand.

Dots that follow the radius

.gen__dot--tl { left: calc(var(--r-tl) * 1px); top: calc(var(--r-vy) * 1px); }
.gen__dot--tr { right: calc(var(--r-tr) * 1px); top: calc(var(--r-vy) * 1px); }
.gen__dot--br { right: calc(var(--r-br) * 1px); bottom: calc(var(--r-vy) * 1px); }
.gen__dot--bl { left: calc(var(--r-bl) * 1px); bottom: calc(var(--r-vy) * 1px); }

Four dark dots sit exactly one radius in from each corner so it stays obvious which handle is moving, and the clip measures 5.82% cumulative moved area, 133.6 average intensity, and 22 of 23 frames with motion.

04Polygon vertex clipping

Six vertex sliders stretch a hexagon in percent coordinates and the picture is cut where they land, so the coordinate list in the code box is the clip-path itself.

Polygon vertex clipping
Open in a new tab

04 · Polygon vertex clipping — Measured area 6.233% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

clip-pathpolygonpointerdown

One coefficient table shared by CSS and JS

// The six vertices are fixed at 60 degree steps. A (dx, dy) coefficient table per vertex — CSS and JS share this one table
// (unit circle coordinates for six directions 60 degrees apart, rounded at the third decimal). Each reaches out from the 50% center by its coefficient
var DIRS = [
  [0, -1], [0.87, -0.5], [0.87, 0.5], [0, 1], [-0.87, 0.5], [-0.87, -0.5]
];

The clipped shape on screen is drawn by calc in the stylesheet while the string you copy is built in JavaScript, and since both read that one table, the picture and the copied code cannot drift apart, and moving the six radii against each other makes the hexagon breathe like a star for 6.23% cumulative moved area, 127.8 average intensity, and 23 of 23 frames with motion.

A vertical coefficient that flattens it

.gen__preview {
  box-sizing: border-box; width: 116px; height: 72px;
  background: $subject-blue;
  clip-path: polygon(
    calc(50% + 0 * var(--v0) * 1%) calc(50% + -1 * var(--v0) * 0.6 * 1%),
    calc(50% + 0.87 * var(--v1) * 1%) calc(50% + -0.5 * var(--v1) * 0.6 * 1%),
    calc(50% + 0.87 * var(--v2) * 1%) calc(50% + 0.5 * var(--v2) * 0.6 * 1%)
  );
}

Every vertical coordinate is multiplied by 0.6 to match the preview box ratio, and the dashed border stays at the uncut size so the part that was taken away is still readable.

05Filter chain presets

Brightness, contrast, saturation, and blur chain up in a single declaration while four preset buttons move all four sliders at once, which is how the tone of a photo gallery gets settled in one sitting.

Filter chain presets
Open in a new tab

05 · Filter chain presets — Measured area 9.446% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

filtersaturatedata-preset

The order of the chain changes the result

filter:
  brightness(calc(var(--f-bri) * 1%))
  contrast(calc(var(--f-con) * 1%))
  saturate(calc(var(--f-sat) * 1%))
  blur(calc(var(--f-blur) * 1px));

Filters apply left to right, so moving blur to the front means contrast lands on an already smeared image, and the clip that catches saturation passing through zero measures 9.45% cumulative moved area, 96.3 average intensity, and 23 of 23 frames with motion.

A preset is four values in one attribute

// Preset buttons — four values go into the properties at once. data-preset shows that the button is where the values come from
Array.prototype.forEach.call(gen.querySelectorAll('.gen__preset'), function (btn) {
  btn.addEventListener('click', function () {
    gen.classList.remove('is-demo');
    btn.dataset.preset.split(',').forEach(function (val, i) {
      gen.style.setProperty(VARS[i], val);
      ranges[i].value = val;
    });
  });
});

Four numbers sit comma-separated inside data-preset, which turns one button into a saved bundle, and the hands-on probe recorded a single click moving all four sliders and the code box in step.

06Grid columns and gap

A minimum-width slider and a gap slider write the repeat() line while auto-fit decides the column count from the width that is actually available.

Grid columns and gap
Open in a new tab

06 · Grid columns and gap — Measured area 10.212% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

grid-template-columnsrepeatminmax

Why there is no column-count slider

.gen__preview {
  display: grid; align-content: start;
  grid-template-columns: repeat(auto-fit, minmax(calc(var(--g-min) * 0.32px), 1fr));
  gap: 0;
  overflow: hidden;
}

auto-fit with minmax never counts columns itself — it lets the minimum width do the counting, so raising the minimum folds the cells into more rows and lowering it pulls them back into one line, recorded as 10.21% cumulative moved area, 169.7 average intensity, and 22 of 23 frames with motion.

The gap carried by the cell's own border

// The gap is given as a transparent border rather than by gap (following 297) — it is part of the cell itself,
// so a fractional px value never breaks the grid's own rounding. Half goes on each side, top and bottom alike
.gen__cell {
  height: 20px; border-radius: $r-xs; box-sizing: border-box;
  border: calc(var(--g-gap) * 0.32px / 2) solid transparent;
  background-clip: padding-box;
}

Splitting the gap into a transparent border on every cell keeps the rhythm even when the value lands on a fractional pixel, and the 0.32 factor in the preview is there only because the stage box is narrower than a real layout.

07Text outline weight

A weight slider and a fill-opacity slider are enough to turn a headline into a sticker, and the fallback for browsers without stroke support comes out of the copy button along with it.

Text outline weight
Open in a new tab

07 · Text outline weight — Measured area 2.958% · moving frames 22/23Original by GODRICH · SCSS and React ship in the zip · See the password

-webkit-text-stroketext-shadowpaint-order

paint-order draws the stroke on the inside

.gen__word {
  color: rgba(255, 247, 230, calc(var(--t-fill) * 1%));
  -webkit-text-stroke: calc(var(--t-sw) * 1px) $subject-ink;
  paint-order: stroke fill;
  font-family: $mono; font-weight: 900; font-size: 44px; line-height: 1;
}

paint-order: stroke fill is an SVG habit that CSS inherited — it keeps the stroke inside the glyph so thin letters do not choke, and dropping the fill to zero hollows the word out for 2.96% cumulative moved area, 123.8 average intensity, and 22 of 23 frames with motion.

The shadow fallback for older browsers

// Imitate the outline with shadows at the same offset in all eight directions (diagonals included)
var shades = [];
for (var dx = -1; dx <= 1; dx++) {
  for (var dy = -1; dy <= 1; dy++) {
    if (dx === 0 && dy === 0) continue;
    shades.push((dx * sw) + 'px ' + (dy * sw) + 'px 0 #17141a');
  }
}
return 'text-shadow: ' + shades.join(', ') + ';';

Where -webkit-text-stroke is missing, eight text-shadow copies at one offset imitate the same thickness, and the copy button hands over both blocks so the choice can be made at the other end.

08Type scale ladder

One base size and one ratio compute six steps, the ladder shows all of them at once, and what falls out is a list of custom properties instead of six numbers somebody typed.

Type scale ladder
Open in a new tab

08 · Type scale ladder — Measured area 42.674% · moving frames 21/23Original by GODRICH · SCSS and React ship in the zip · See the password

clampMath.pow--step-3

Only the number of multiplications differs

.gen__step--1 { font-size: calc(var(--s-base) * 0.6px); }
.gen__step--2 { font-size: calc(var(--s-base) * 0.6px * var(--s-ratio) * 0.01); }
.gen__step--3 { font-size: calc(var(--s-base) * 0.6px * var(--s-ratio) * var(--s-ratio) * 0.0001); }
.gen__step--4 { font-size: calc(var(--s-base) * 0.6px * var(--s-ratio) * var(--s-ratio) * var(--s-ratio) * 0.000001); }

CSS calc has no exponent, so the ratio is written one more time at each step and one formula with a different count of multiplications lets the whole ladder breathe from a single value, which is why six lines resizing together push this clip to 42.67% cumulative moved area, 153.5 average intensity, and 21 of 23 frames with motion.

What gets copied is a rem list

// Base size and ratio — the five-step ladder follows by itself through calc multiplication (the same formula as in the scss).
// There are only two properties: --s-base (integer px) and --s-ratio (integer percent)
var base = readVar('--s-base');
var ratio = readVar('--s-ratio') / 100;
var lines = [];
for (var i = 1; i <= 6; i++) {
  lines.push('--step-' + i + ': ' + (Math.round(base * Math.pow(ratio, i - 1) * 100) / 100 / 16).toFixed(3) + 'rem;');
}

The copy button hands over --step-1 through --step-6 as one :root block, and the 0.6 factor in the preview is display scaling that keeps six steps inside the stage without touching the ratio between them.

09Timing value preview

Duration, delay, and iteration sit on three sliders and the ball beside them restarts with the new numbers, which is the quickest way to settle how long an entrance should take.

Timing value preview
Open in a new tab

09 · Timing value preview — Measured area 3.245% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

animation-durationanimation-delaygetAnimations

Longhand instead of the shorthand

// Longhand properties — this avoids the trap where the shorthand resets delay
animation-name: ballRun;
animation-duration: calc(var(--a-dur) * 1ms);
animation-delay: calc(var(--a-delay) * 1ms);
animation-iteration-count: var(--a-iter);
animation-timing-function: $easing;
animation-direction: alternate;
animation-fill-mode: both;

The animation shorthand returns every sub-property you leave out to its initial value, which wipes out a delay set elsewhere, so this panel uses longhand from the start and the ball keeps traveling through every change at 3.25% cumulative moved area, 146.0 average intensity, and 23 of 23 frames with motion.

Restarting the ball on every change

// When a value changes, start the ball's animation again — grab it with getAnimations and put the clock back to 0
function restartBall() {
  ball.getAnimations().forEach(function (a) {
    a.cancel();
  });
  // After cancel, an animation of the same name is created again — the CSS sheet is still there
}

Dragging a slider cancels the animation caught by getAnimations(), and because the CSS rule is still in the sheet, it is immediately recreated under the same name, starting from zero with the new duration.

How many sliders does a box shadow generator need?

Five values decide a box shadow — x, y, blur, spread, and color — but the color is usually already fixed by the design system, so this panel starts with four sliders. What matters more is the order of looking and reading: sweeping two pixels at a time to find where the card lifts beats guessing 16px and correcting afterwards. The second layer in the stack is the first one halved, so four adjustable axes really are enough.

Nine panels, their controls, and what they print

item controls code it prints
01 shadow x, y, blur, spread box-shadow in two layers
02 gradient angle, stop A, stop B linear-gradient
03 corners four corner radii, vertical radius border-radius with a slash
04 polygon six vertex radii clip-path: polygon
05 filter brightness, contrast, saturation, blur a filter chain
06 grid minimum width, gap grid-template-columns and gap
07 text outline weight, fill -webkit-text-stroke and color
08 type base, ratio --step-1 through --step-6
09 timing duration, delay, iteration the animation- longhands

Every number travels as a custom property, which is why a panel's preview, thumb, and code box cannot fall out of step, and the hands-on probe recorded value, preview, and code moving together in all nine (run/073/_조작실측.json).

Where it breaks — the trap

All three breaks below turned up after the panels already looked finished, which is the reason every zip ships the vanilla build and the React build together under the password ycejvu79. The first thing to break was 320px. Seven of the nine sat comfortably at the desktop width of 480px, then pushed their content out to somewhere between 331px and 444px on a phone. The cause was the slider itself: input[type=range] carries an intrinsic minimum size, and it shoved the grid track and the card along with it. The two panels carrying the most sliders — the border radius generator at five and the polygon one at six — concentrated the most pressure in a single card. The fix came in three layers: the track became repeat(2, minmax(0, 1fr)) so its minimum could fall to zero, the slider cell and the input both took min-width: 0, and the card cut its own automatic minimum as a grid item of the stage. All nine now sit inside a 320×200 body.

The second was the digits in the code box. A number painted by a CSS counter is not readable through textContent, and asking for the computed value hands back the string counter(v) instead. The probe ended up proving the change with a pixel hash of the code box — a textbook case of the machine reading something other than what the eye sees. The third was the clipboard: inside an iframe navigator.clipboard.writeText returns a rejected promise that try/catch never sees, so each panel attaches a .catch that falls back to copying from an off-screen textarea, and all nine files carry that same pattern.

Accessibility

With prefers-reduced-motion: reduce in effect, the value cycle in the grid preview stops, the ball in 09 rests at its starting position, and nothing is removed from the screen. The sliders are native input[type=range], so arrow keys move them — the hands-on probe was driven entirely from the keyboard — and every one carries an aria-label, with the focus ring drawn through focus-visible in all nine stylesheets. After a click the copy button swaps its own label for a moment, so the result is something you can see rather than only assume.

FAQ

Do both lines of the copied box shadow have to be used?

No. The first line is the main shadow and the second is the half-value shadow under it. One card is usually fine with the first line alone, and the pair is for when the floating feeling has to be stronger.

Why does the code box show a counter instead of a plain number?

Because the digits are painted by counter() reading the same custom property as the preview — that is what keeps them in sync without a redraw. The copy button does not copy that text — it builds the finished declaration in JavaScript, so what lands in your clipboard is ordinary CSS.

Can the output of the clip path generator sit on a background image?

Yes. clip-path cuts the element itself, background included, so pasting the copied polygon() onto a div that carries the picture as its background works exactly as it does on an <img>. What it cannot do is clip one background layer while leaving another alone.

Sources

Enter the archive password

The password is inside this article. You will find it as you read.