- Published on
Mastering CSS Variables: Create a Stunning, Responsive City Skyline from Scratch
- Authors
- Name
- Md Nasim Sheikh
- @nasimStg
'Mastering CSS Variables: Create a Stunning, Responsive City Skyline from Scratch'
Discover the magic of CSS Custom Properties (Variables) by building a beautiful, responsive city skyline. This step-by-step tutorial will guide you through creating dynamic, themeable, and maintainable CSS art.
Table of Contents
- 'Mastering CSS Variables: Create a Stunning, Responsive City Skyline from Scratch'
- What Are CSS Variables (and Why Should You Care)?
- Section 1: The Blueprint - Setting Up Our HTML Foundation
- Section 2: Painting the Scene - Global Variables & Basic Styles
- Section 3: Constructing the Buildings with Local Variables
- Section 4: Adding Details - Windows via Repeating Gradients
- Section 5: Bringing the City to Life - Day/Night Theming
- Section 6: A City for Every Screen - Responsive Design
- Section 7: Best Practices & Advanced Tips
- Conclusion: The City of the Future is Variable
Ever stared at a complex CSS stylesheet and felt a wave of dread? Hundreds of lines, repeated color codes, and media queries that look like a tangled mess. We've all been there. But what if I told you there's a native CSS feature that can bring order to this chaos, making your code cleaner, more powerful, and ridiculously fun to write?
Enter CSS Custom Properties, more affectionately known as CSS Variables. They're not just a tool; they're a paradigm shift in how we think about and write CSS. To truly appreciate their power, we're not just going to talk about them. We're going to build something beautiful and complex: a dynamic, themeable, and responsive 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 harness CSS variables to make your future projects more maintainable and efficient.
What Are CSS Variables (and Why Should You Care)?
Before we lay the first brick of our city, let's get acquainted with our primary tool. CSS Custom Properties are, simply put, entities defined by you that contain specific values to be reused throughout a document. Think of them as variables in a programming language, but for CSS.
The Syntax is simple:
Declaring a variable: You declare a variable using a custom property name that begins with two hyphens (
--
), and assign it a value. It's best practice to declare global variables in the:root
pseudo-class.:root { --main-bg-color: #f5f5f5; --primary-text-color: #333; --base-font-size: 16px; }
Using a variable: You use the variable with the
var()
function.body { background-color: var(--main-bg-color); color: var(--primary-text-color); font-size: var(--base-font-size); }
So, why is this a game-changer?
- DRY (Don't Repeat Yourself): No more
Ctrl+F
to find and replace that one hex code you used 37 times. Change it in one place, and it updates everywhere. - Enhanced Readability:
color: var(--brand-primary-color);
is infinitely more meaningful thancolor: #4A90E2;
. - Effortless Theming: As we'll see, creating a day/night mode or different color themes becomes trivial.
- Simplified Responsiveness: Tweak layouts and sizes across breakpoints by just changing a variable's value, not a dozen properties.
- Global and Local Scopes: You can set variables globally on
:root
or locally on any element, giving you fine-grained control.
Now that we know the what and why, let's get to the how. Time to build our city.
Section 1: The Blueprint - Setting Up Our HTML Foundation
Every great structure starts with a solid blueprint. For our CSS art, the HTML is our foundation. We'll keep it lean and semantic. Our entire cityscape will live inside a single <div>
.
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 Variables Skyline</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div class="skyline-container">
<div class="sky"></div>
<div class="city">
<!-- Buildings will be added here -->
<div class="building bldg-1"></div>
<div class="building bldg-2"></div>
<div class="building bldg-3"></div>
<div class="building bldg-4"></div>
<div class="building bldg-5"></div>
</div>
</div>
<div class="theme-switcher">
<button data-theme="day">Day</button>
<button data-theme="night">Night</button>
</div>
</body>
</html>
What's going on here?
.skyline-container
: The main wrapper for our scene..sky
: This will be our background, where we'll create a beautiful gradient sky..city
: This will act as a container for all our buildings, allowing us to position them easily using Flexbox..building
: A generic class for all buildings..bldg-1
,.bldg-2
, etc.: Modifier classes to give each building a unique look. This is where we'll see the power of overriding variables..theme-switcher
: A simple pair of buttons we'll hook up later to demonstrate dynamic theming.
Section 2: Painting the Scene - Global Variables & Basic Styles
With our HTML in place, let's create our style.css
file and start painting. First, we'll define our global variables in the :root
pseudo-class. This acts as our central configuration panel.
/* In style.css */
:root {
/* DAY THEME - Default */
--sky-top: #6DD5FA;
--sky-bottom: #2980B9;
--bldg-color1: #8e9eab;
--bldg-color2: #5c6b73;
--bldg-color3: #34495e;
--window-on: #f1c40f;
--window-off: #4a6e8a;
/* Base Dimensions */
--bldg-base-width: 100px;
--bldg-base-height: 200px;
--window-size: 8px;
--window-spacing: 12px;
}
body {
margin: 0;
font-family: sans-serif;
background-color: #111;
}
.skyline-container {
position: relative;
width: 100%;
height: 100vh;
overflow: hidden;
}
.sky {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(to bottom, var(--sky-top), var(--sky-bottom));
transition: background 0.5s ease-in-out;
}
.city {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 50%; /* Adjust as needed */
display: flex;
align-items: flex-end; /* Align buildings to the bottom */
justify-content: space-around;
}
Already, you can see the benefits. We've defined our entire day-theme color palette and base dimensions in one place. Our .sky
class uses var(--sky-top)
and var(--sky-bottom)
to create its gradient. If we want to change the sky's color, we only need to edit those two variables.
Notice the transition
on the .sky
's background. This will give us a smooth fade when we switch themes later.
Section 3: Constructing the Buildings with Local Variables
Now for the fun part: building our skyscrapers. We'll start with a base .building
style and then use our modifier classes to create unique structures by overriding variables at a local scope.
This is a crucial concept: variables defined on a more specific selector override those on a less specific one.
Add the following to your style.css
:
.building {
/* Use local variables, with fallbacks to global base values */
--width: var(--bldg-base-width);
--height: var(--bldg-base-height);
--bg-color: var(--bldg-color1);
width: var(--width);
height: var(--height);
background-color: var(--bg-color);
position: relative; /* For pseudo-elements later */
margin: 0 10px;
}
/* --- Individual Building Styles --- */
.bldg-1 {
--height: calc(var(--bldg-base-height) * 1.8);
--width: calc(var(--bldg-base-width) * 1.2);
--bg-color: var(--bldg-color1);
}
.bldg-2 {
--height: calc(var(--bldg-base-height) * 2.5);
--width: calc(var(--bldg-base-width) * 0.9);
--bg-color: var(--bldg-color2);
align-self: center; /* Example of overriding flex alignment */
}
.bldg-3 {
--height: calc(var(--bldg-base-height) * 2.2);
--width: calc(var(--bldg-base-width) * 1.5);
--bg-color: var(--bldg-color3);
}
.bldg-4 {
--height: calc(var(--bldg-base-height) * 1.5);
--width: calc(var(--bldg-base-width) * 1.1);
--bg-color: var(--bldg-color1);
}
.bldg-5 {
--height: calc(var(--bldg-base-height) * 2.8);
--width: calc(var(--bldg-base-width) * 1.0);
--bg-color: var(--bldg-color2);
}
Look at how clean that is! The .building
class sets up default properties by defining and then immediately using local variables like --width
and --height
. The modifier classes (.bldg-1
, .bldg-2
, etc.) do only one thing: they provide new values for those variables.
We're also introducing calc()
. The ability to perform calculations with CSS variables is a superpower. We can create proportional relationships (--height: calc(var(--bldg-base-height) * 1.8);
) that make our design system more robust and easier to adjust.
At this point, you should see a basic skyline of different colored and sized rectangles against a gradient sky.
Section 4: Adding Details - Windows via Repeating Gradients
An unlit building is just a block. Let's add windows. We could add dozens of <div>
s for each window, but that's inefficient. A much smarter way is to use a repeating-linear-gradient
to create a grid pattern.
And, you guessed it, we'll control the entire pattern with variables!
Update your .building
class:
.building {
/* ... existing variables ... */
--window-color: var(--window-off); /* Default to off */
/* ... existing properties ... */
/* Window Grid */
background-image: repeating-linear-gradient(
var(--bg-color),
var(--bg-color) var(--window-spacing),
var(--window-color) var(--window-spacing),
var(--window-color) calc(var(--window-spacing) + var(--window-size)),
var(--bg-color) calc(var(--window-spacing) + var(--window-size))
),
repeating-linear-gradient(
to right,
var(--bg-color),
var(--bg-color) var(--window-spacing),
var(--window-color) var(--window-spacing),
var(--window-color) calc(var(--window-spacing) + var(--window-size)),
var(--bg-color) calc(var(--window-spacing) + var(--window-size))
);
background-blend-mode: multiply; /* Blend the horizontal and vertical lines */
transition: background-color 0.5s ease-in-out;
}
This code might look intimidating, but it's just drawing two overlapping patterns of stripes:
- A vertical pattern of transparent and colored stripes.
- A horizontal pattern of transparent and colored stripes.
When layered, they form a grid. The key is that every part of this grid—the size of the windows (--window-size
), the spacing between them (--window-spacing
), and their color (--window-color
)—is controlled by our variables.
Now, you can easily create different window patterns for different buildings just by overriding these variables in the modifier classes:
/* Example: A building with larger, more spaced-out windows */
.bldg-3 {
--height: calc(var(--bldg-base-height) * 2.2);
--width: calc(var(--bldg-base-width) * 1.5);
--bg-color: var(--bldg-color3);
--window-size: 12px;
--window-spacing: 20px;
}
Just by adding two lines, we've completely changed the building's facade. This is maintainable CSS at its finest.
Section 5: Bringing the City to Life - Day/Night Theming
This is the moment where CSS Variables truly flex their muscles. We're going to implement a day/night theme switcher without changing a single property in our existing .sky
or .building
classes. We'll only change variable values.
First, let's define the night theme's color palette. We'll scope these new variable values to an attribute selector, [data-theme="night"]
. We'll apply this attribute to the <body>
tag with JavaScript.
Add this to your style.css
:
/* NIGHT THEME */
[data-theme="night"] {
--sky-top: #0f2027;
--sky-bottom: #203a43;
--bldg-color1: #2c3e50;
--bldg-color2: #1a252f;
--bldg-color3: #1c1c1c;
--window-color: var(--window-on); /* Turn the lights on! */
}
That's it. That's all the CSS you need for the theme.
When the <body>
has data-theme="night"
, these variables will override the default ones in :root
. Our .sky
will automatically use the new gradient colors. And most magically, the --window-color
in our .building
class will now default to var(--window-on)
, effectively turning on all the lights in the city.
Now, let's add the JavaScript to make our buttons work. Create a script.js
file (and remember to link it in your HTML: <script src="script.js" defer></script>
).
// In script.js
document.addEventListener('DOMContentLoaded', () => {
const themeSwitcher = document.querySelector('.theme-switcher');
const body = document.body;
themeSwitcher.addEventListener('click', (event) => {
if (event.target.tagName === 'BUTTON') {
const theme = event.target.dataset.theme;
body.setAttribute('data-theme', theme);
}
});
// Set a default theme on load
body.setAttribute('data-theme', 'day');
});
This simple script listens for clicks on our theme buttons and sets the data-theme
attribute on the <body>
accordingly. Go ahead and try it! You should see your city smoothly transition from a bright day scene to a moody, illuminated night scene.
Section 6: A City for Every Screen - Responsive Design
How do we make our skyline responsive? In the old days, we'd have to write a bunch of new rules inside a media query, overriding heights, widths, margins, etc.
With variables, it's much cleaner. We just redefine the base variables that control our layout.
@media (max-width: 768px) {
:root {
--bldg-base-width: 70px;
--bldg-base-height: 150px;
--window-size: 5px;
--window-spacing: 8px;
}
.city {
justify-content: flex-start; /* Let buildings overflow */
padding: 0 20px;
}
}
@media (max-width: 480px) {
:root {
--bldg-base-width: 50px;
--bldg-base-height: 120px;
}
}
By simply changing four variables inside the media query, we've adjusted the entire scale of our city. All the calc()
functions in our building modifiers will automatically use these new base values. The window grids will resize. Everything just works.
This approach keeps your media queries concise and your design logic centralized in your :root
selector.
Section 7: Best Practices & Advanced Tips
You've now mastered the fundamentals. Here are a few extra tips to take your CSS variable skills to the next level.
Fallback Values: The
var()
function can take a second argument: a fallback value. This is used if the variable isn't defined. It's great for defensive coding or when working with components that might exist outside a system..component { /* If --accent-color is not set, it will use 'tomato' */ background-color: var(--accent-color, tomato); }
JavaScript Interaction: You can get and set CSS variables directly from JavaScript. This is incredibly powerful for creating dynamic UIs that respond to user input (like a color picker).
// Set a variable document.documentElement.style.setProperty('--sky-top', '#ff0000'); // Get a variable const currentColor = getComputedStyle(document.documentElement).getPropertyValue('--sky-top');
Animating Custom Properties: While you can't directly animate most custom properties, you can register them with
@property
to tell the browser their type, initial value, and whether they can be inherited. This allows for smooth animations of properties that weren't previously animatable, like gradients!@property --sky-top { syntax: '<color>'; inherits: false; initial-value: #6DD5FA; } /* Now transitions on this property will be much smoother */
Conclusion: The City of the Future is Variable
We did it! We've gone from a blank canvas to a fully-realized, responsive, and themeable city skyline. More importantly, we've seen firsthand how CSS Variables transform the development process.
We learned to:
- Centralize design tokens like colors and sizes in
:root
. - Create variations of components by overriding variables locally.
- Use
calc()
with variables for proportional scaling. - Implement complex patterns like window grids in a manageable way.
- Build an entire theme system by simply redefining a handful of variables.
- Write cleaner, more maintainable media queries.
CSS Variables aren't just a feature; they're a foundational part of modern CSS architecture. They encourage systematic thinking and empower you to build complex, dynamic interfaces with surprising simplicity.
So, what's next? Try adding more buildings. Create a 'sunset' theme with orange and purple hues. Use pseudo-elements to add antennas and spires to your buildings. The city is yours to expand. Happy coding!