DEV Community

Cover image for How I Made This Realistic Red Switch (Pure CSS)
Yoav Kadosh
Yoav Kadosh

Posted on

How I Made This Realistic Red Switch (Pure CSS)

I created this pen last week after stumbling across this 3D Red Switch design on Dribble. Since then it gained a lot of popularity (much more than I have anticipated!), and a few people have asked me to write a tutorial about how I made it.

I've used various different CSS techniques in the making of this, including gradients, 3D transformations, animations, and transitions. In this tutorial, I will go over some of these techniques in depth.

Simulating a State

This is probably the oldest trick in the book. A switch has 2 states - on and off, but CSS has no way of maintaining such a state.

To solve this, we can use one of the native HTML elements. Since we only need to maintain 2 states, a checkbox element is a great choice. We can use the :checked CSS selector to apply CSS based on whether the checkbox is checked or not.

We wrap the whole thing in a <label/> to link the click event of the entire element to the checkbox, and we hide the checkbox itself using CSS.

<label class="switch">
  <input type="checkbox" checked/>
</label>
Enter fullscreen mode Exit fullscreen mode
.switch {
  input {
    display: none;
  }
}
Enter fullscreen mode Exit fullscreen mode

One issue with this is that we can't apply CSS to the <label/> itself based on the checkbox state, since there's no "ancestor selector" in CSS. Because of that, I placed all the switch elements after the checkbox and used the adjacent sibling selector (+) to apply CSS to them.

<label class="switch">
  <input type="checkbox" checked/>
  <div class="button"></div>
</label>
Enter fullscreen mode Exit fullscreen mode
.switch {
  input {
    display: none;

    &:checked + .button {
      // Apply some CSS to .button when the checkbox is checked
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

If you need to simulate an element with more than 2 states, you can use other HTML elements like radio buttons (<input type="radio"/>). Some people take this technique to the next level and create an entire game using CSS only! Check out this collection of pure CSS games on CodePen for some inspiration.

Making The Black Frame

Making the black frame using CSS box-shadow

I used box-shadow to simulate the button's frame. box-shadow is a very powerful CSS property, because it allows you to stack multiple shadow effects separated by a comma.

I used a set of 5 shadow effects to create the frame, and a border-radius property to make the shadow round in the corners. Here's the breakdown:

.switch {
  border-radius: 5px;
  box-shadow: 
    0 0 10px 2px rgba(black, 0.2), // The surrounding shadow (first layer)
    0 0 1px 2px black, // The surrounding shadow (second layer)
    inset 0 2px 2px -2px white, // The top white "shine"
    inset 0 0 2px 15px #47434c, // The light gray frame
    inset 0 0 2px 22px black; // The internal black shadow
}
Enter fullscreen mode Exit fullscreen mode

Making The 3D Button Shape

I used CSS transformations and transitions to make the button appear 3-dimensional.

The button itself is made of 3 divs (1 div and 2 pseudo-elements to be precise), that are perpendicular to each other, as illustrated below:

Making a 3D red switch using CSS

.button {
  &::before {
    height: 50px;
    width: 100%;
    transform: rotateX(-90deg);
  }
  &::after {
    height: 50px;
    width: 100%;
    transform: translateY(50px) rotateX(-90deg);
  }
}
Enter fullscreen mode Exit fullscreen mode

Then, I rotated the entire button 25 degrees, and used transform-origin to set the pivot point away from the div, to make the button appear as if it is rotating around some point deeper inside the button, rather than around the div:

Making a 3D red switch using CSS

.switch {
  perspective: 700px;

  .button {
    $rotation: 25deg;
    $pivot-distance: 20px;
    transform-origin: center center -#{$pivot-distance};
     transform: translateZ($pivot-distance) rotateX(-$rotation);
    transform-style: preserve-3d;
  }
}
Enter fullscreen mode Exit fullscreen mode

Making The Animation

I used CSS transitions to rotate the switch back and forth. I wanted the transition to appear realistic, by starting slowly and ending quickly. I could've used one of the native easing functions, like ease-in, but that wasn't producing the right animation, so I used a custom cubic-bezier() easing function instead:

transition: all 0.3s cubic-bezier(1, 0, 1, 1);
Enter fullscreen mode Exit fullscreen mode

Chrome DevTools transition cubic-bezier easing

This curve means that the transition starts slowly, and ends quickly, just like a real switch that slowly turns until it "clicks" towards the end.

Making The I/O Characters

Making the I/O characters using CSS gradients

There are multiple tricks I could've used to create the I/O characters. I could've used real letters and apply styling to them, or use a special font. But since those are pretty simple characters to draw, I decided to use gradients to make them.

CSS gradients are amazing, but I had no idea how powerful they are until I came across this great article about CSS Drawings.

The true power of gradients comes from the fact that they are considered as "images" by CSS, and therefore can benefit from the power of the background property. backgrounds in CSS can not only be stacked (like shadows), but they can also have custom position and size!

This means that you can do pretty much everything with CSS gradients. If you want to understand just how far you can take it, check out https://a.singlediv.com/ (every art piece on that site is made with a single div).

The syntax is pretty simple:

background: <image> <position> / <size>
Enter fullscreen mode Exit fullscreen mode

You can stack multiple gradients with commas, and add background-repeat: no-repeat to prevent the gradients from repeating:

.image {
  background:
    <image> <position> / <size>,
    <image> <position> / <size>,
    <image> <position> / <size>;
  background-repeat: no-repeat;
}
Enter fullscreen mode Exit fullscreen mode

I made the characters using a background with 2 gradients.
For the "I" character, I used an all-white linear-gradient(), and made it narrow and long. For the "O" character I used a radial-gradient() with 4 color stops, from transparent to white and back to transparent.

background:
  linear-gradient(white, white) 50% 20% / 5% 20%, // White vertical line ("I")
  radial-gradient(circle, transparent 50%, white 52%, white 70%, transparent 72%) 50% 80% / 33% 25%; // White circle ("O")
Enter fullscreen mode Exit fullscreen mode

If you take a look at the radial-gradient(), you'll notice that there's a 2% gap between each color stop:

radial-gradient(
  transparent 50%, 
  white 52%, 
  white 70%, 
  transparent 72%
)
Enter fullscreen mode Exit fullscreen mode

This makes the different colors blend together, instead of having a crisp, pixelated transition. To illustrate this, take a look at the image below:

CSS gradients - blending colors between color stops

This is an inherent CSS gradient behavior - it creates a smooth blend between the colors when there's a gap between the color stops.

Making The "LED" Gradient

Making the LED light using CSS gradients and animations

As seen in the picture above, I stacked 2 gradients to achieve a look of an LED bulb hidden behind a semi-transparent red plastic with small round bumps on it.

I had to use 2 elements, one for each gradient, since the first gradient had to be none-repeating, and the second one had to be repeating. Additionally, I wanted to make the light "flicker", so I had to separate them.

The first element is the .light element, where I used a radial-gradient() to illustrate a red LED light, with a brighter center (the center is orange, while the surroundings are red).

.light {
  background-image: radial-gradient(
    adjust-hue(lighten($color, 20%), 35), // Orange
    $color 40%, // Red
    transparent 70%
  );
}
Enter fullscreen mode Exit fullscreen mode

Don't be alarmed by adjust-hue() and lighten(), I will cover those in the next part. For now, just consider them as hex colors.

The second element is the .dots element, where I used a repeating radial-gradient() with a transparent center to create a matrix of round-shaped bumps.

.dots {
  background-image: 
    radial-gradient(transparent 30%, rgba(darken($color, 35%), 0.7) 70%);
    background-size: 10px 10px;
}
Enter fullscreen mode Exit fullscreen mode

Finally, I used animation to create the flickering effect:

Alt Text

.light {
  animation: flicker 0.2s infinite 0.3s;
}

@keyframes flicker {
  0% {opacity: 1}
  80% {opacity: 0.8}
  100% {opacity: 1}
}
Enter fullscreen mode Exit fullscreen mode

Controlling The Color Through A Variable

As this pen gained popularity, some people asked to see it in different colors. Initially, I had hardcoded colors throughout my CSS, so I changed those to SASS variables for simple configuration.

However, I wanted the main color to be easily configurable, so having multiple color variables wasn't good enough. I needed to control all of the colors & shading through a single variable.

In order to achieve that, I used SASS's built-in color functions: lighten(), darken() and adjust-hue() (SassMe is a nice tool for visualizing the output of these functions).

lighten() and darken() are pretty self-explanatory. They make a given color lighter or darker based on the given percentage. For example, lighten(black, 50%) will mix black with 50% white, producing a gray color.

Making a color lighter using SASS lighten(), visualized by https://sassme.jim-nielsen.com/

However, for the LED light, lighten() and darken() were not enough, since the center of the light was orange, while the surroundings were red. That's not a different color shade, it's a different color altogether.

That's where adjust-hue() comes in handy. It lets you change the color's hue property by a given degree.

A color's hue is the color's position on the color wheel and can be represented by a single numerical value, usually in degrees (0 - 360).

Color hue scale

So I used adjust-hue() to "rotate" the color's hue property 35 degrees to the right:

adjust-hue($color, 35)
Enter fullscreen mode Exit fullscreen mode

Producing this:

Adjusting the color's hue property using SASS adjust-hue(), visualized by https://sassme.jim-nielsen.com/

So if the color is red, to rotated color will be orange. But if the color is green the rotated color becomes blue!

Adjusting the color's hue property using SASS adjust-hue(), visualized by https://sassme.jim-nielsen.com/

So now, you can control all the colors in the switch via a single variable $color:

Changing the switch main color

Summary

This tutorial turned out to be a bit longer than I anticipated, and it may appear a bit overwhelming at first to see all the different techniques and tricks that were used for making this switch. But when you break it down to its basic elements, these techniques are pretty simple to comprehend.

I hope I was able to provide some insight into the development process, and I hope you learned some new CSS techniques.

Discussion (31)

Collapse
jh3y profile image
Jhey Tompkins

Looks great! One tiny thing I'd change 🤏

Move the input outside the label and make the clickable area of the label half the size of the switch. That way, you can't click the side that's pressed in. Then it'll be "realistic" 😉

Collapse
ykadosh profile image
Yoav Kadosh Author

Yes, others have suggested this also, however, I think that this is problematic in terms of accessibility since the user expects the whole switch to be clickable.

Collapse
jh3y profile image
Jhey Tompkins

I think that would be the least of the concerns in making this accessible 😅

However, If you want to take it a step further though, divs aren't allowed as children of label 👍
Switch them to span and use display to style them up.

Thread Thread
ykadosh profile image
Yoav Kadosh Author

Yes, I guess you're right... 😅
And that's a good point regarding the divs 👍

Collapse
devggaurav profile image
Gaurav

Nicely explained!

Collapse
moopet profile image
Ben Sinclair

This looks really nice (though it's not "pure CSS").

If I could make one suggestion? Drop the opacity on the characters to maybe 0.8 or so so they let a little of the light through.

Collapse
ykadosh profile image
Yoav Kadosh Author

Thanks for the feedback, interesting suggestion.
May I ask why you don't consider this as pure CSS?

Collapse
moopet profile image
Ben Sinclair

It needs a bunch of HTML to back it up? There are several DIVs there whose sole purpose is presentation.

Thread Thread
ykadosh profile image
Yoav Kadosh Author

Pure CSS does not mean zero HTML, you need HTML elements to apply the CSS to.

Thread Thread
moopet profile image
Ben Sinclair

I know what you're getting at. But pure CSS to me means using the elements that are already there, and no more.

If you're allowing adding HTML, then you may as well say, "Let's have 1920x1080 DIVs and give them all unique IDs", because then "Pure CSS" will let you do full HD video, and it all becomes meaningless.

To me, pure CSS is what you could apply to an existing page without having to ask the authors to change their markup.

Thread Thread
samjarman profile image
Sam Jarman 👨🏼‍💻

I’ve always thought developers meant “no JavaScript” when they say pure CSS/ HTML.

Semantics aside, I’d say this is pretty impressive right?

Thread Thread
moopet profile image
Ben Sinclair

Nobody says that though, they say "Pure CSS". It's kind of a trend.

Yes, it looks really good!

Collapse
7tonshark profile image
Elliot Nelson

Nice work! Never knew you could stack multiple box-shadow like that.

Collapse
shivams136 profile image
Shivam Sharma

Amazing step-by-step guide. I have learned a lot. Thanks.
Since when I saw your last article, I was curious to have a step-by-step guide and you provided better than my expectations.

Collapse
ykadosh profile image
Yoav Kadosh Author

Thanks, Shivam! 🙏

Collapse
muzammilaalpha profile image
muzammilaalpha

Nicely explained!

Collapse
nikmors profile image
Nik Moraitis

This is genius! Great job. I will definitely try this one. The level of realism you are giving it it's amazing. Thank you for sharing this!

Collapse
ocdalex profile image
Alex Walker

Very cool and some of the possibilities of css very well explained, have bookmarked for future reference.

Collapse
pris_stratton profile image
pris stratton

Wow! Incredible. Reminds me a bit of HAL9000 too.

Collapse
juhoheinonen profile image
juhoheinonen

Looks wonderful. Thanks!

Collapse
bartosz profile image
Bartosz Wójcik

One of the best if not THE BEST looking CSS gems I've seen in my entire life! Well done Yoav!

Collapse
ykadosh profile image
Yoav Kadosh Author

Thank you!

Collapse
codeboi profile image
Timothy Rowell

Nice, I saw this on Codepen I know you made it.

Collapse
abhishekcghosh profile image
Abhishek Ghosh

Impressive!

Collapse
samjarman profile image
Sam Jarman 👨🏼‍💻

This is really impressive. Nice.

Collapse
andreidascalu profile image
Andrei Dascalu

Amazing! Actual result aside, as far as tutorials/showcases go, this point but point approach works great for teaching.

Collapse
pabloverdier profile image
pablo verdier

Genial! and very well explained, thnks

Collapse
christiangroeber profile image
Christian Gröber

I LOVE THIS SO MUCH!

Spent a good 15 minutes just toggling the switch, it's so satisfying! Great job

Collapse
possan profile image
Per-Olov Jernberg

I have been clicking this for too long now, beautiful attention to detail!

Collapse
ayhan_yildiz_d7b5f946bb72 profile image
ayhan yildiz

impressive work! very interesting that so many feedback for improvement 🙄