Skip to content

Performance Patterns

This chapter isn’t about inventing new animation tricks.
It’s about wiring the tricks you already know to CSS variables so you can:

  • Tune how far things move
  • Adjust how strong shadows and glows feel
  • Nudge timings faster or slower

All without touching layout, and without digging through 20 selectors every time you want “just a little less.”

Variables become performance-friendly dials.


Instead of hard-coding every shadow:

.card {
box-shadow: 0 24px 60px rgba(15, 23, 42, 0.45);
}

Use variables as intensity knobs:

:root {
--shadow-strength: 0.45; /* unitless */
}
.card {
box-shadow: 0 24px 60px rgba(15, 23, 42, var(--shadow-strength));
}
.card--subtle {
--shadow-strength: 0.22;
}
.card--hero {
--shadow-strength: 0.7;
}

Same pattern works for:

  • Blur strength: filter: blur(calc(1px * var(--blur-strength)));
  • Glow opacity
  • Scale factors: transform: scale(var(--hover-scale));
  • Timing tweaks: transition-duration: calc(160ms * var(--duration-multiplier));

Variables control how intense the effect feels; the underlying CSS stays the same.


You can also hide your durations and delays behind variables:

:root {
--duration-fast: 140ms;
--duration-base: 220ms;
--duration-slow: 420ms;
}
.button {
transition:
transform var(--duration-fast) ease-out,
box-shadow var(--duration-fast) ease-out;
}
.modal {
transition:
opacity var(--duration-base) ease-out,
transform var(--duration-base) ease-out;
}

When the team decides “everything should feel snappier,” you bump --duration-* in one place instead of hunting through every component.

You can even combine knobs:

:root {
--duration-base: 180ms;
--duration-multiplier: 1;
}
.card {
transition:
transform calc(var(--duration-base) * var(--duration-multiplier)) ease-out,
box-shadow calc(var(--duration-base) * var(--duration-multiplier)) ease-out;
}

Set --duration-multiplier to 0.8 for “speed run” mode, or 1.2 for a calmer feel.


This MiniMo gives you a live performance control panel:

  • Buttons to switch hover intensity (Subtle / Default / Extra)
  • CSS variables drive:
    • Lift distance
    • Shadow strength
    • Transition duration

JavaScript only updates the knobs — CSS handles the effect.

Core idea behind the MiniMo:

:root {
--lift-distance: 10px;
--shadow-strength: 0.35;
--transition-ms: 160ms;
}
.card {
transform: translateY(0);
box-shadow:
0 0 0 1px rgba(15, 23, 42, 0.85),
0 20px 40px rgba(15, 23, 42, var(--shadow-strength));
transition:
transform var(--transition-ms) ease-out,
box-shadow var(--transition-ms) ease-out;
}
.card:hover {
transform: translateY(calc(-1 * var(--lift-distance)));
box-shadow:
0 0 0 1px rgba(15, 23, 42, 0.9),
0 26px 70px rgba(15, 23, 42, calc(var(--shadow-strength) + 0.18));
}
const root = document.documentElement;
const presets = {
subtle: {
'--lift-distance': '6px',
'--shadow-strength': '0.22',
'--transition-ms': '130ms',
},
default: {
'--lift-distance': '10px',
'--shadow-strength': '0.35',
'--transition-ms': '160ms',
},
extra: {
'--lift-distance': '16px',
'--shadow-strength': '0.55',
'--transition-ms': '190ms',
},
};
function applyPreset(name) {
const preset = presets[name];
for (const [key, value] of Object.entries(preset)) {
root.style.setProperty(key, value);
}
}

Nothing in the selector logic changes — the card just responds to new variable values.


Now that you’ve got the dials, here are the rails they should stay on.

Rule you can quote in class:

“If it makes the browser re-measure or re-flow the page, don’t animate it.”

Avoid animating things like:

  • top, left, right, bottom
  • width, height
  • margin, padding
  • border-width

Those trigger layout and paint work every frame.

Prefer:

  • transform: translate / scale / rotate
  • opacity
  • Occasional box-shadow or filter (used lightly)

Then plug your variables into those properties:

:root {
--lift-distance: 14px;
--hover-scale: 1.02;
}
.card {
transform: translateY(0) scale(1);
transition:
transform var(--transition-ms) ease-out,
box-shadow var(--transition-ms) ease-out;
}
.card:hover {
transform: translateY(calc(-1 * var(--lift-distance)))
scale(var(--hover-scale));
}

Your variables control the feel; transforms + opacity keep things smooth.


For elements that you know will animate often on hover, you can hint:

.card {
will-change: transform, box-shadow;
}

This tells the browser “keep this ready to animate.”
Use it carefully — on too many elements it can hurt performance.

Good candidates:

  • Main call-to-action buttons
  • Reusable interactive cards
  • Always-on nav elements with subtle motion

Bad candidates:

  • Dozens of list items
  • Rarely-hovered one-off elements

You can frame these patterns as:

  • Design dials — variables for intensity, speed, and lift
  • Performance rails — transforms + opacity as the main animation track
  • JS as the operator — controls the dials, doesn’t redraw the scene

Students don’t need to memorize every restriction.
They just need the mantra:

“Animate transforms and opacity. Put intensity behind variables.
Let JS move the knobs, not the layout.”


With these patterns, CSS variables stop being “just theme colors” and become a performance-aware control system for your UI.

Next stop in the series: plugging these ideas into bigger layouts — nav bars, cards, and whole page sections that feel smooth no matter how extra the glow gets.