Creating the DigitalOcean Logo in 3D With CSS

Howdy y’all! Unless you’ve been living under a rock (and maybe even then), you’ve undoubtedly heard the news that CSS-Tricks, was acquired by DigitalOcean. Congratulations to everyone! 🥳

As a little hurrah to commemorate the occasion, I wanted to create the DigitalOcean logo in CSS. I did that, but then took it a little further with some 3D and parallax. This also makes for quite a good article because the way I made the logo uses various pieces from previous articles I’ve written. This cool little demo brings many of those concepts together.

So, let’s dive right in!

Creating the DigitalOcean logo

We are going to “trace” the DigitalOcean logo by grabbing an SVG version of it from

<svg role="img" viewbox="0 0 24 24" xmlns=""> <title>DigitalOcean</title> <path d="M12.04 0C5.408-.02.005 5.37.005 11.992h4.638c0-4.923 4.882-8.731 10.064-6.855a6.95 6.95 0 014.147 4.148c1.889 5.177-1.924 10.055-6.84 10.064v-4.61H7.391v4.623h4.61V24c7.86 0 13.967-7.588 11.397-15.83-1.115-3.59-3.985-6.446-7.575-7.575A12.8 12.8 0 0012.039 0zM7.39 19.362H3.828v3.564H7.39zm-3.563 0v-2.978H.85v2.978z"></path>

Being mindful that we’re taking this 3D, we can wrap our SVG in a .scene element. Then we can use the tracing technique from my “Advice for Advanced CSS Illustrations” article. We are using Pug so we can leverage its mixins and reduce the amount of markup we need to write for the 3D part.

- const SIZE = 40
.scene svg(role='img' viewbox='0 0 24 24' xmlns='') title DigitalOcean path(d='M12.04 0C5.408-.02.005 5.37.005 11.992h4.638c0-4.923 4.882-8.731 10.064-6.855a6.95 6.95 0 014.147 4.148c1.889 5.177-1.924 10.055-6.84 10.064v-4.61H7.391v4.623h4.61V24c7.86 0 13.967-7.588 11.397-15.83-1.115-3.59-3.985-6.446-7.575-7.575A12.8 12.8 0 0012.039 0zM7.39 19.362H3.828v3.564H7.39zm-3.563 0v-2.978H.85v2.978z') .logo(style=`--size: ${SIZE}`) .logo__arc.logo__arc--inner .logo__arc.logo__arc--outer .logo__square.logo__square--one .logo__square.logo__square--two .logo__square.logo__square--three

The idea is to style these elements so that they overlap our logo. We don’t need to create the “arc” portion of the logo as we’re thinking ahead because we are going to make this logo in 3D and can create the arc with two cylinder shapes. That means for now all we need is the containing elements for each cylinder, the inner arc, and the outer arc.

Check out this demo that lays out the different pieces of the DigitalOcean logo. If you toggle the “Explode” and hover elements, you can what the logo consists of.

If we wanted a flat DigitalOcean logo, we could use a CSS mask with a conic gradient. Then we would only need one “arc” element that uses a solid border.

.logo__arc--outer { border: calc(var(--size) * 0.1925vmin) solid #006aff; mask: conic-gradient(transparent 0deg 90deg, #000 90deg); transform: translate(-50%, -50%) rotate(180deg);

That would give us the logo. The “reveal” transitions a clip-path that shows the traced SVG image underneath.

Check out my “Advice for Complex CSS Illustrations” article for tips on working with advanced illustrations in CSS.

Extruding for the 3D

We have the blueprint for our DigitalOcean logo, so it’s time to make this 3D. Why didn’t we create 3D blocks from the start? Creating containing elements, makes it easier to create 3D via extrusion.

We covered creating 3D scenes in CSS in my “Learning to Think in Cubes Instead of Boxes” article. We are going to use some of those techniques for what we’re making here. Let’s start with the squares in the logo. Each square is a cuboid. And using Pug, we are going to create and use a cuboid mixin to help generate all of them.

mixin cuboid() .cuboid(class!=attributes.class) if block block - let s = 0 while s < 6 .cuboid__side - s++

Then we can use this in our markup:

.scene .logo(style=`--size: ${SIZE}`) .logo__arc.logo__arc--inner .logo__arc.logo__arc--outer .logo__square.logo__square--one +cuboid().square-cuboid.square-cuboid--one .logo__square.logo__square--two +cuboid().square-cuboid.square-cuboid--two .logo__square.logo__square--three +cuboid().square-cuboid.square-cuboid--three

Next, we need the styles to display our cuboids. Note that cuboids have six sides, so we’re styling those with the nth-of-type() pseudo selector while leveraging the vmin length unit to keep things responsive.

.cuboid { width: 100%; height: 100%; position: relative;
.cuboid__side { filter: brightness(var(--b, 1)); position: absolute;
.cuboid__side:nth-of-type(1) { --b: 1.1; height: calc(var(--depth, 20) * 1vmin); width: 100%; top: 0; transform: translate(0, -50%) rotateX(90deg);
.cuboid__side:nth-of-type(2) { --b: 0.9; height: 100%; width: calc(var(--depth, 20) * 1vmin); top: 50%; right: 0; transform: translate(50%, -50%) rotateY(90deg);
.cuboid__side:nth-of-type(3) { --b: 0.5; width: 100%; height: calc(var(--depth, 20) * 1vmin); bottom: 0; transform: translate(0%, 50%) rotateX(90deg);
.cuboid__side:nth-of-type(4) { --b: 1; height: 100%; width: calc(var(--depth, 20) * 1vmin); left: 0; top: 50%; transform: translate(-50%, -50%) rotateY(90deg);
.cuboid__side:nth-of-type(5) { --b: 0.8; height: 100%; width: 100%; transform: translate3d(0, 0, calc(var(--depth, 20) * 0.5vmin)); top: 0; left: 0;
.cuboid__side:nth-of-type(6) { --b: 1.2; height: 100%; width: 100%; transform: translate3d(0, 0, calc(var(--depth, 20) * -0.5vmin)) rotateY(180deg); top: 0; left: 0;

We are approaching this in a different way from how we have done it in past articles. Instead of applying height, width, and depth to a cuboid, we are only concerned with its depth. And instead of trying to color each side, we can make use of filter: brightness to handle that for us.

If you need to have cuboids or other 3D elements as a child of a side using filter, you may need to shuffle things. A filtered side will flatten any 3D children.

The DigitalOcean logo has three cuboids, so we have a class for each one and are styling them like this:

.square-cuboid .cuboid__side { background: hsl(var(--hue), 100%, 50%);
.square-cuboid--one { /* 0.1925? It's a percentage of the --size for that square */ --depth: calc((var(--size) * 0.1925) * var(--depth-multiplier));
.square-cuboid--two { --depth: calc((var(--size) * 0.1475) * var(--depth-multiplier));
.square-cuboid--three { --depth: calc((var(--size) * 0.125) * var(--depth-multiplier));

…which gives us something like this:

You can play with the depth slider to extrude the cuboids as you wish! For our demo, we’ve chosen to make the cuboids true cubes with equal height, width, and depth. The depth of the arc will match the largest cuboid.

Now for the cylinders. The idea is to create two ends that use border-radius: 50%. Then, we can use many elements as the sides of the cylinder to create the effect. The trick is positioning all the sides.

There are various approaches we can take to create the cylinders in CSS. But, for me, if this is something I can foresee using many times, I’ll try and future-proof it. That means making a mixin and some styles I can reuse for other demos. And those styles should try and cater to scenarios I could see popping up. For a cylinder, there is some configuration we may want to consider:

  • radius
  • sides
  • how many of those sides are displayed
  • whether to show one or both ends of the cylinder

Putting that together, we can create a Pug mixin that caters to those needs:

mixin cylinder(radius = 10, sides = 10, cut = [5, 10], top = true, bottom = true) - const innerAngle = (((sides - 2) * 180) / sides) * 0.5 - const cosAngle = Math.cos(innerAngle * (Math.PI / 180)) - const side = 2 * radius * Math.cos(innerAngle * (Math.PI / 180)) //- Use the cut to determine how many sides get rendered and from what point .cylinder(style=`--side: ${side}; --sides: ${sides}; --radius: ${radius};` class!=attributes.class) if top .cylinder__end.cylinder__segment.cylinder__end--top if bottom .cylinder__end.cylinder__segment.cylinder__end--bottom - const [start, end] = cut - let i = start while i < end .cylinder__side.cylinder__segment(style=`--index: ${i};`) - i++

See how //- is prepended to the comment in the code? That tells Pug to ignore the comment and leave it out from the compiled HTML markup.

Why do we need to pass the radius into the cylinder? Well, unfortunately, we can’t quite handle trigonometry with CSS calc() just yet (but it is coming). And we need to work out things like the width of the cylinder sides and how far out from the center they should project. The great thing is that we have a nice way to pass that information to our styles via inline custom properties.

.cylinder( style=` --side: ${side}; --sides: ${sides}; --radius: ${radius};` class!=attributes.class

An example use for our mixin would be as follows:

+cylinder(20, 30, [10, 30])

This would create a cylinder with a radius of 20, 30 sides, where only sides 10 to 30 are rendered.

Then we need some styling. Styling the cylinders for the DigitalOcean logo is pretty straightforward, thankfully:

.cylinder { --bg: hsl(var(--hue), 100%, 50%); background: rgba(255,43,0,0.5); height: 100%; width: 100%; position: relative;
.cylinder__segment { filter: brightness(var(--b, 1)); background: var(--bg, #e61919); position: absolute; top: 50%; left: 50%;
.cylinder__end { --b: 1.2; --end-coefficient: 0.5; height: 100%; width: 100%; border-radius: 50%; transform: translate3d(-50%, -50%, calc((var(--depth, 0) * var(--end-coefficient)) * 1vmin));
.cylinder__end--bottom { --b: 0.8; --end-coefficient: -0.5;
.cylinder__side { --b: 0.9; height: calc(var(--depth, 30) * 1vmin); width: calc(var(--side) * 1vmin); transform: translate(-50%, -50%) rotateX(90deg) rotateY(calc((var(--index, 0) * 360 / var(--sides)) * 1deg)) translate3d(50%, 0, calc(var(--radius) * 1vmin));

The idea is that we create all the sides of the cylinder and put them in the middle of the cylinder. Then we rotate them on the Y-axis and project them out by roughly the distance of the radius.

There’s no need to show the ends of the cylinder in the inner part since they’re already obscured. But we do need to show them for the outer portion. Our two-cylinder mixin use look like this:

.logo(style=`--size: ${SIZE}`) .logo__arc.logo__arc--inner +cylinder((SIZE * 0.61) * 0.5, 80, [0, 60], false, false).cylinder-arc.cylinder-arc--inner .logo__arc.logo__arc--outer +cylinder((SIZE * 1) * 0.5, 100, [0, 75], true, true).cylinder-arc.cylinder-arc--outer

We know the radius from the diameter we used when tracing the logo earlier. Plus, we can use the outer cylinder ends to create the faces of the DigitalOcean logo. A combination of border-width and clip-path comes in handy here.

.cylinder-arc--outer .cylinder__end--top,
.cylinder-arc--outer .cylinder__end--bottom { /* Based on the percentage of the size needed to cap the arc */ border-width: calc(var(--size) * 0.1975vmin); border-style: solid; border-color: hsl(var(--hue), 100%, 50%); --clip: polygon(50% 0, 50% 50%, 0 50%, 0 100%, 100% 100%, 100% 0); clip-path: var(--clip);

We’re pretty close to where we want to be!

There is one thing missing though: capping the arc. We need to create some ends for the arc, which requires two elements that we can position and rotate on the X or Y-axis:

.scene .logo(style=`--size: ${SIZE}`) .logo__arc.logo__arc--inner +cylinder((SIZE * 0.61) * 0.5, 80, [0, 60], false, false).cylinder-arc.cylinder-arc--inner .logo__arc.logo__arc--outer +cylinder((SIZE * 1) * 0.5, 100, [0, 75], true, true).cylinder-arc.cylinder-arc--outer .logo__square.logo__square--one +cuboid().square-cuboid.square-cuboid--one .logo__square.logo__square--two +cuboid().square-cuboid.square-cuboid--two .logo__square.logo__square--three +cuboid().square-cuboid.square-cuboid--three .logo__cap.logo__cap--top .logo__cap.logo__cap--bottom

The arc’s capped ends will assume the height and width based on the end’s border-width value as well as the depth of the arc.

.logo__cap { --hue: 10; position: absolute; height: calc(var(--size) * 0.1925vmin); width: calc(var(--size) * 0.1975vmin); background: hsl(var(--hue), 100%, 50%);
.logo__cap--top { top: 50%; left: 0; transform: translate(0, -50%) rotateX(90deg);
.logo__cap--bottom { bottom: 0; right: 50%; transform: translate(50%, 0) rotateY(90deg); height: calc(var(--size) * 0.1975vmin); width: calc(var(--size) * 0.1925vmin);

We’ve capped the arc!

Throwing everything together, we have our DigitalOcean logo. This demo allows you to rotate it in different directions.

But there’s still one more trick up our sleeve!

Adding a parallax effect to the logo

We’ve got our 3D DigitalOcean logo but it would be neat if it was interactive in some way. Back in November 2021, we covered how to create a parallax effect with CSS custom properties. Let’s use that same technique here, the idea being that the logo rotates and moves by following a user’s mouse cursor.

We do need a dash of JavaScript so that we can update the custom properties we need for a coefficient that sets the logo’s movement along the X and Y-axes in the CSS. Those coefficients are calculated from a user’s pointer position. I’ll often use GreenSock so I can use gsap.utils.mapRange. But, here is a vanilla JavaScript version of it that implements mapRange:

const mapRange = (inputLower, inputUpper, outputLower, outputUpper) => { const INPUT_RANGE = inputUpper - inputLower const OUTPUT_RANGE = outputUpper - outputLower return value => outputLower + (((value - inputLower) / INPUT_RANGE) * OUTPUT_RANGE || 0)
} const BOUNDS = 100 const update = ({ x, y }) => { const POS_X = mapRange(0, window.innerWidth, -BOUNDS, BOUNDS)(x) const POS_Y = mapRange(0, window.innerHeight, -BOUNDS, BOUNDS)(y)'--coefficient-x', POS_X)'--coefficient-y', POS_Y)
} document.addEventListener('pointermove', update)

The magic happens in CSS-land. This is one of the major benefits of using custom properties this way. JavaScript is telling CSS what’s happening with the interaction. But, it doesn’t care what CSS does with it. That’s a rad decoupling. I use this JavaScript snippet in so many of my demos for this very reason. We can create different experiences simply by updating the CSS.

How do we do that? Use calc() and custom properties that are scoped directly to the .scene element. Consider these updated styles for .scene:

.scene { --rotation-y: 75deg; --rotation-x: -14deg; transform: translate3d(0, 0, 100vmin) rotateX(-16deg) rotateY(28deg) rotateX(calc(var(--coefficient-y, 0) * var(--rotation-x, 0deg))) rotateY(calc(var(--coefficient-x, 0) * var(--rotation-y, 0deg)));

The makes the scene rotate on the X and Y-axes based on the user’s pointer movement. But we can adjust this behavior by tweaking the values for --rotation-x and --rotation-y.

Each cuboid will move its own way. They are able to move on either the X, Y, or Z-axis. But, we only need to define one transform. Then we can use scoped custom properties to do the rest.

.logo__square { transform: translate3d( calc(min(0, var(--coefficient-x, 0) * var(--offset-x, 0)) * 1%), calc((var(--coefficient-y) * var(--offset-y, 0)) * 1%), calc((var(--coefficient-x) * var(--offset-z, 0)) * 1vmin) );
.logo__square--one { --offset-x: 50; --offset-y: 10; --offset-z: -2;
.logo__square--two { --offset-x: -35; --offset-y: -20; --offset-z: 4;
.logo__square--three { --offset-x: 25; --offset-y: 30; --offset-z: -6;

That will give you something like this:

And we can tweak these to our heart’s content until we get something we’re happy with!

Adding an intro animation to the mix

OK, I fibbed a bit and have one final (I promise!) way we can enhance our work. What if we had some sort of intro animation? How about a wave or something that washes across and reveals the logo?

We could do this with the pseudo-elements of the body element:

:root { --hue: 215; --initial-delay: 1; --wave-speed: 2;
} body:after,
body:before { content: ''; position: absolute; height: 100vh; width: 100vw; background: hsl(var(--hue), 100%, calc(var(--lightness, 50) * 1%)); transform: translate(100%, 0); animation-name: wave; animation-duration: calc(var(--wave-speed) * 1s); animation-delay: calc(var(--initial-delay) * 1s); animation-timing-function: ease-in;
body:before { --lightness: 85; animation-timing-function: ease-out;
@keyframes wave { from { transform: translate(-100%, 0); }

Now, the idea is that the DigitalOcean logo is hidden until the wave washes over the top of it. For this effect, we’re going to animate our 3D elements from an opacity of 0. And we’re going to animate all the sides to our 3D elements from a brightness of 1 to reveal the logo. Because the wave color matches that of the logo, we won’t see it fade in. Also, using animation-fill-mode: both means that our elements will extend the styling of our keyframes in both directions.

This requires some form of animation timeline. And this is where custom properties come into play. We can use the duration of our animations to calculate the delays of others. We looked at this in my “How to Make a Pure CSS 3D Package Toggle” and “Animated Matryoshka Dolls in CSS” articles.

:root { --hue: 215; --initial-delay: 1; --wave-speed: 2; --fade-speed: 0.5; --filter-speed: 1;
} .cylinder__segment,
.logo__cap { animation-name: fade-in, filter-in; animation-duration: calc(var(--fade-speed) * 1s), calc(var(--filter-speed) * 1s); animation-delay: calc((var(--initial-delay) + var(--wave-speed)) * 0.75s), calc((var(--initial-delay) + var(--wave-speed)) * 1.15s); animation-fill-mode: both;
} @keyframes filter-in { from { filter: brightness(1); }
} @keyframes fade-in { from { opacity: 0; }

How do we get the timing right? A little tinkering and making use of the “Animations Inspector” in Chrome’s DevTool goes a long ways. Try adjusting the timings in this demo:

You may find that the fade timing is unnecessary if you want the logo to be there once the wave has passed. In that case, try setting the fade to 0. And in particular, experiment with the filter and fade coefficients. They relate to the 0.75s and 1.15s from the code above. It’s worth adjusting things and having a play in Chrome’s Animation Inspector to see how things time in.

That’s it!

Putting it all together, we have this neat intro for our 3D DigitalOcean logo!

And, of course, this only one approach to create the DigitalOcean logo in 3D with CSS. If you see other possibilities or perhaps something that can be optimized further, drop a link to your demo in the comments!

Congratulations, again, to the CSS-Tricks team and DigitalOcean for their new partnership. I’m excited to see where things go with the acquisition. One thing is for sure: CSS-Tricks will continue to inspire and produce fantastic content for the community. 😎