Published on

Harnessing the Power of CSS Variables: A Step-by-Step Guide to Building a Dynamic City Skyline

Authors

'Harnessing the Power of CSS Variables: A Step-by-Step Guide to Building a Dynamic City Skyline'

Unlock the full potential of modern CSS with this in-depth tutorial. Learn how to use CSS Custom Properties (Variables) to build a beautiful, responsive, and themeable city skyline from scratch.

Table of Contents

Harnessing the Power of CSS Variables: Build a City Skyline

Pure CSS art has a certain magic to it. With just a stylesheet, developers can transform a blank page into anything from a photorealistic portrait to a whimsical cartoon. But often, the code behind these creations can look like an impenetrable wall of repeated values, complex selectors, and painstaking pixel-perfect adjustments. What if I told you there's a modern CSS feature that makes this process not only easier but also more powerful, maintainable, and even dynamic?

Enter CSS Variables, or more formally, CSS Custom Properties.

Today, we're not just going to talk about CSS Variables in theory. We're going to roll up our sleeves and use them for a practical, hands-on project: building a beautiful, responsive, and themeable city skyline. By the end of this tutorial, you'll not only have a cool piece of CSS art but also a deep understanding of how to leverage variables in your everyday front-end development work.

Let's build something amazing.

So, What Are CSS Variables and Why Should You Care?

Before we lay the first brick of our city, let's understand our primary tool. If you've ever used a CSS preprocessor like Sass or Less, the concept of variables isn't new. You define a value once and reuse it everywhere. It's a cornerstone of writing DRY (Don't Repeat Yourself) code.

The "old way" in vanilla CSS was painful. Imagine you have a brand color, #3498db, used in 50 different places in your stylesheet. If the client decides to change it, you're stuck with a tedious and error-prone find-and-replace mission.

CSS Custom Properties solve this natively in the browser. They bring the power of variables to CSS without needing a preprocessor.

Here's the basic syntax:

/* Defining a variable */
:root {
  --primary-color: #3498db;
}

/* Using a variable */
.button {
  background-color: var(--primary-color);
}

But here's the game-changer: CSS Variables are live and respect the cascade. Unlike Sass variables, which are compiled away into static values, CSS variables exist in the DOM. This means they can be updated with CSS rules (like in a media query) or even manipulated with JavaScript in real-time. This opens up a world of possibilities for theming, responsive design, and dynamic interactivity.

In short, you should care because they lead to:

  • Cleaner Code: var(--main-brand-font) is far more readable than 'Helvetica Neue', Arial, sans-serif.
  • Easier Maintenance: Change a value in one place (:root), and it updates everywhere.
  • Powerful Theming: Creating a dark/light mode becomes trivial.
  • Dynamic Possibilities: They bridge the gap between CSS and JavaScript.

Now, let's put this power to use.

Section 1: Setting the Scene - The HTML Structure

Good news: our HTML is going to be incredibly simple. The real star of the show is the CSS. We just need a few div elements to act as our canvas and building blocks.

Create an index.html file and paste in the following structure:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>CSS Variable Skyline</title>
  <link rel="stylesheet" href="style.css">
</head>
<body>

  <div class="scene">
    <div class="sky"></div>
    <div class="city">
      <div class="building b1"></div>
      <div class="building b2"></div>
      <div class="building b3"></div>
      <div class="building b4"></div>
      <div class="building b5"></div>
      <div class="building b6"></div>
    </div>
  </div>

  <button id="theme-toggle">Toggle Day/Night</button>

  <script src="script.js"></script>
</body>
</html>

We have:

  • A main .scene container to hold everything.
  • A .sky element for our background.
  • A .city container to hold our buildings.
  • Several .building divs, each with a unique class (b1, b2, etc.) so we can style them individually.
  • A button that we'll use later to toggle our day/night theme.

That's it! Now for the fun part.

Section 2: Laying the Foundation with Global Variables

Let's create our style.css file. The first thing we'll do is define our global variables inside the :root pseudo-class. This makes them available everywhere in our document. Think of this as our project's master control panel.

/* style.css */

:root {
  /* ===== THEME COLORS (DAY) ===== */
  --sky-top: #6dd5ed;
  --sky-bottom: #2193b0;
  --building-color: #4a4a4a;
  --building-silhouette: #333;
  --window-off-color: #555;
  --window-on-color: #fce3a7;

  /* ===== CORE METRICS ===== */
  --base-unit: 1vmin; /* A responsive unit based on the smaller viewport dimension */
  --scene-width: calc(var(--base-unit) * 180);
  --scene-height: calc(var(--base-unit) * 80);

  /* ===== TRANSITIONS ===== */
  --theme-transition-duration: 1.2s;
}

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background-color: #f0f0f0;
  font-family: sans-serif;
}

Notice we're using vmin for our --base-unit. This is a fantastic unit for this kind of illustrative work because it's relative to the smaller of the viewport's width or height. This ensures our skyline scales proportionally without getting distorted on different screen sizes.

Styling the Scene and Sky

Now let's use these variables to style our main containers.

.scene {
  position: relative;
  width: var(--scene-width);
  max-width: 95%;
  height: var(--scene-height);
  overflow: hidden;
  border: calc(var(--base-unit) * 0.5) solid #333;
  box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}

.sky {
  position: absolute;
  width: 100%;
  height: 100%;
  background: linear-gradient(to bottom, var(--sky-top), var(--sky-bottom));
  transition: background var(--theme-transition-duration) ease-in-out;
}

.city {
  position: absolute;
  bottom: 0;
  width: 100%;
  height: 50%; /* Buildings will grow from the bottom up into this space */
}

Already, you can see the benefit. Our sky is a beautiful gradient defined by --sky-top and --sky-bottom. To change the time of day, we'll only need to change those two variables, and the transition property will handle the smooth fade.

Section 3: Constructing the City, One Variable at a Time

This is where the real magic happens. We'll create a base style for all buildings and then use local variables to give each one a unique size and position.

First, the generic .building class:

.building {
  position: absolute;
  bottom: 0;
  background-color: var(--building-color);
  transition: background-color var(--theme-transition-duration) ease-in-out;

  /* Local variables will be defined here by each specific building class */
  width: calc(var(--b-width, 10) * var(--base-unit)); /* Default width of 10 units */
  height: calc(var(--b-height, 20) * var(--base-unit)); /* Default height of 20 units */
  left: calc(var(--b-left, 0) * var(--base-unit)); /* Default left position of 0 */
}

Take a close look at the width, height, and left properties. We're using var() with a fallback value, e.g., var(--b-width, 10). This means "use the --b-width variable if it's defined in this scope; otherwise, use 10 as the value." This makes our base class robust.

Now, let's define our first building, .b1, by simply setting its local variables.

/* ===== Individual Building Styles ===== */

.b1 {
  --b-width: 15;
  --b-height: 40;
  --b-left: 5;
  z-index: 3;
}

That's it! We're not repeating width, height, or left. We're just providing the data. The generic .building class does all the heavy lifting. This is a powerful, component-based way of thinking, brought to you by CSS Variables.

Let's add the rest of our buildings. See how clean and readable this is?

.b2 {
  --b-width: 12;
  --b-height: 65;
  --b-left: 22;
  z-index: 4;
  background-color: var(--building-silhouette); /* A darker building for depth */
}

.b3 {
  --b-width: 20;
  --b-height: 55;
  --b-left: 38;
  z-index: 2;
}

.b4 {
  --b-width: 18;
  --b-height: 75;
  --b-left: 65;
  z-index: 5; 
  background-color: var(--building-silhouette);
}

.b5 {
  --b-width: 25;
  --b-height: 50;
  --b-left: 88;
  z-index: 3;
}

.b6 {
  --b-width: 15;
  --b-height: 35;
  --b-left: 118;
  z-index: 1;
}

We've used z-index to create a sense of depth and even overridden the background-color on a couple of buildings to create silhouettes. Our city is starting to take shape!

Section 4: Bringing the City to Life with Windows

A city isn't complete without windows. We could add hundreds of divs for windows, but that would bloat our HTML. Instead, we'll use a clever CSS trick with repeating background gradients.

Let's add this to our .building class:

.building {
  /* ... existing styles ... */

  /* ===== WINDOWS ===== */
  --window-size: calc(var(--base-unit) * 1.2);
  --window-gap: calc(var(--base-unit) * 0.8);
  --window-combined-size: calc(var(--window-size) + var(--window-gap));

  /* The magic: a grid of 'off' windows */
  background-image: linear-gradient(var(--window-off-color), var(--window-off-color));
  background-size: var(--window-size) var(--window-size);
  background-repeat: repeat;
  background-position: var(--window-gap) var(--window-gap);

  /* We add the building color back on top */
  box-shadow: inset 0 0 0 1000px var(--building-color);
  transition: box-shadow var(--theme-transition-duration) ease-in-out;
}

This is a bit complex, so let's break it down:

  1. We define some local variables for window sizing and spacing.
  2. background-image and background-size create a single square of color, --window-off-color.
  3. background-repeat: repeat; tiles this square across the entire element.
  4. background-position adds the initial offset, creating the gaps between windows.
  5. The problem now is that our entire building is covered in windows. The box-shadow: inset... trick is the solution. An enormous inset shadow, using our var(--building-color), effectively paints the building's color back on top of the window grid, leaving only the small window squares visible.

It's a clever hack, but it works beautifully and is incredibly performant.

Section 5: From Day to Night - Theming with a Single Class

This is the moment where the power of CSS Variables truly shines. We want to switch our entire scene from day to night with the click of a button. All we need to do is define a .night class and, within it, redefine the variables we set in :root.

Add this to the bottom of your style.css:

/* ===================================== */
/* ============ NIGHT THEME ============ */
/* ===================================== */

.scene.night {
  /* ===== Redefine variables for the night theme ===== */
  --sky-top: #0c1445;
  --sky-bottom: #3b2f63;
  --building-color: #1e1e1e;
  --building-silhouette: #111;
  --window-off-color: #2c2c2c;
}

When the .scene element also has the .night class, these new variable values will take precedence. Because our entire component is built on variables, the sky, buildings, and windows will all update automatically. And thanks to the transition properties we set up earlier, the change will be beautifully animated.

Turning the Lights On

For the final touch, let's turn on some lights at night. We'll use pseudo-elements on specific buildings to overlay a grid of 'on' windows. This gives us more control than trying to randomly switch individual background tiles.

/* Add a shared pseudo-element style for lit windows */
.b1::after, .b3::after, .b5::after {
  content: '';
  position: absolute;
  top: 0; left: 0; right: 0; bottom: 0;
  
  /* Use the same grid logic, but with the 'on' color */
  background-image: linear-gradient(var(--window-on-color), var(--window-on-color));
  background-size: var(--window-size) var(--window-size);
  background-repeat: repeat;
  background-position: var(--window-gap) var(--window-gap);

  opacity: 0; /* Hidden by default (in day time) */
  transition: opacity var(--theme-transition-duration) ease-in-out;
}

/* When it's night, turn on the lights! */
.scene.night .b1::after,
.scene.night .b3::after,
.scene.night .b5::after {
  opacity: 1;
}

We're applying this effect only to buildings .b1, .b3, and .b5 to create some variety. The ::after pseudo-element sits on top of the building, and by default, it's completely transparent. When the .night class is added, we simply fade it in to opacity: 1, revealing the glowing yellow windows.

The JavaScript Toggle

The final piece of the puzzle is the JavaScript to toggle the .night class. Create a script.js file with these few lines:

// script.js

const themeToggle = document.getElementById('theme-toggle');
const scene = document.querySelector('.scene');

themeToggle.addEventListener('click', () => {
  scene.classList.toggle('night');
});

This tiny script is all we need. It listens for a click on our button and adds or removes the .night class from the .scene element. CSS handles the rest.

Section 6: Best Practices and Final Thoughts

We've successfully built a responsive, themeable city skyline, and hopefully, you've seen just how powerful CSS Variables can be. Here are a few best practices to carry forward:

  1. Scope Intelligently: Use :root for global, theme-level variables (colors, fonts, global spacing). Use local variables (defined on a specific class) for component-specific variations (like our building dimensions).
  2. Name Sensibly: Adopt a clear naming convention. A prefix like --comp- for components (e.g., --card-padding) or --theme- for themes can prevent conflicts and improve readability.
  3. Use Fallbacks: var(--my-var, #000) is your safety net. It ensures that if a variable isn't defined for some reason, your layout won't break entirely.
  4. Combine with calc(): As we saw, calc() and variables are a match made in heaven. They allow you to perform calculations on your dynamic values, which is impossible with preprocessors.
  5. Embrace prefers-color-scheme: For an even better user experience, you can use the prefers-color-scheme media query to automatically apply your .night theme based on the user's OS settings.
@media (prefers-color-scheme: dark) {
  .scene {
    /* Your night theme variable overrides go here */
    --sky-top: #0c1445;
    --sky-bottom: #3b2f63;
    /* ...etc */
  }

  /* You might need to hide the toggle or sync it with the state */
}

Conclusion: Your City, Your Rules

We started with a few empty divs and, using the power of CSS Custom Properties, we constructed an entire cityscape. We saw how global variables make theming a breeze and how local variables allow for powerful, DRY component variations.

This project is just the beginning. CSS Variables are a fundamental part of the modern web developer's toolkit. They aren't just for colors; they are for sizes, timings, layout values, and any other property you can think of. They encourage a more modular, maintainable, and dynamic approach to writing CSS.

I encourage you to take this project and make it your own. Add more buildings with unique features. Create a sunrise or sunset theme. Animate the windows to flicker on and off. The foundation we've built is scalable and ready for your creativity.

So, what will you build next?