I work with oil paints a lot, mostly creating urban landscapes. An experiment crossed my mind to try to recreate these paintings using only CSS. Surprisingly, the process of painting with CSS is very similar to painting with oil; start out in broad strokes working down to more detail, then refactor again and again.
Working from the F train photograph shown above, I’ll run through some of the important steps I learned along the way to recreate this image in CSS. Here is the finished product for reference.
I set up a .paper
class to contain all of the pieces of the painting.
<div class="paper"></div>
* {
box-sizing: border-box;
}
body {
margin: 0;
padding: 0;
height: 100vh;
width: 100vw;
background: #fff;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
font-family: 'Nanum Gothic Coding', monospace;
font-size: 11px;
}
.paper {
height: 50vmin;
width: 70vmin;
background: #cc0000;
position: relative;
z-index: -1000;
}
For the body, I used viewport units to set the height and width to 100vh and 100vw, which is relative to the percentage width of the browser window. I used flexbox to center the paper.
I set the height and width of .paper
to a landscape size, set the z-index to ensure it’s always the bottom element, and set its position to relative so the paper becomes the baseline where all the painting pieces attach themselves.
Finally, I gave the background a red color so I would know the difference between what is the background and what is not (since there will be a lot of different colors). You can also add an overflow: hidden
so any excess elements don’t come off the paper, but I opted to create everything to fit.
I used SCSS variables to make life easier. Since I was working from a photograph, I used Mac’s Digital Color Meter to create the color variables. I tried to name all of the colors to represent the physical object, like $floor
and $seat
instead of just $grey
and $red
. This is very important, especially as your painting grows in size.
Start painting from background to foreground, otherwise, your head will explode from reordering divs and tinkering with z-index. This may not always be possible, and you’ll inevitably end up having to shuffle some pieces around, but keep the back to front approach in mind.
I approached the CSS the same way I approach a painting. I break down everything into modules — floor, floor shadow, seat, wall, etc. Think in terms of only using one color per piece, the way a silk screener or block printer works. Where it made sense, I grouped pieces together as parent/children, for example, I had a back-wall-container and within it, all of the back wall pieces.
The basic approach looks like this:
<div class="paper">
<div class="floor"></div>
</div>
And then the CSS:
.floor {
position: absolute;
height: 4vmin;
width: 70vmin;
background: linear-gradient(to right, #222, #222, $floor);
right: 0vmin;
bottom: 0vmin;
z-index: -2;
}
I set the position to be absolute, so it’s using the paper as its guide. For each piece, I started off giving it an arbitrary background color (something easy to see, like green) then eyeballing the size based on the image. I used vmin, a percentage of the width or height, whichever one is smaller. In this case, to me, the floor looked about 4vmin high which ran the entire width of the painting.
Next, I positioned the floor on the paper. In this case to the bottom right. Most of these are trial and error, and you end up moving the pieces around like a game of Tetris until they fit into the right spot. Finally, I used my $floor
color variable along with a linear-gradient in this case to create a little shadow.
I used :before
and :after
to add highlights or shadows to some elements. The nice thing about pseudo-element selectors is that they allow positioning another element relative to itself. For the “Do not lean on door” sign on the subway door (which I’ve never seen anyone actually follow), I created the sign background, then used :after
to create the white top border line. I opted to not add the sign text. Here is the CSS:
.door-sign {
position: absolute;
width: 10vmin;
height: 1.25vmin;
background: $door-sign;
right: 8.75vmin;
top: 22vmin;
}
.door-sign:after {
content: ' ';
position: absolute;
width: 9vmin;
height: .15vmin;
background: $door-sign-border;
left: .5vmin;
top: .2vmin;
opacity: .5;
}
I set the door sign like all the other elements with height, width, background, and positioned it on the paper. Next, I used :after
, setting the content to an empty string (we don’t want to add any content, just a shape). Then I positioned the white border relative to the door sign. This makes painting certain elements easier because you don’t have to rethink where the element needs to be positioned on the entire page because it inherits from its parent.
Obviously, everything can’t be a square, rectangle or circle, so I had to create all kinds of different shapes using borders and transforms. Here is a great resource from CSS-Tricks to understand how to create complicated shapes, using before/after and borders.
For the more complicated shapes, for example, the top section of the middle seat, I had to use other shapes and layer them on top to create the desired result.
This section of the seat was a rectangle with a border-radius at the top and right. To create the tapering effect at the top left and right, I created a triangle “shim” and made it the same background as the seat base:
.seat-tri-1 {
position: absolute;
width: 0;
height: 0;
border-left: .75vmin solid transparent;
border-right: .75vmin solid transparent;
border-top: 10vmin solid $seat-base;
top: 25.5vmin;
left: 13.95vmin;
z-index: 10;
}
To create the triangle, I set the width and height to 0, then three of the borders, two of which will be transparent, one will be the color, depending on the direction of the triangle. Finally, I positioned the shim between seats one and two. If you have trouble finding the shim, change the border-top color to something visible, like green.
We could refactor the painting to make the code more reusable using a mixin:
@mixin paint($height, $width, $background, $left: auto, $right: auto, $top: auto, $bottom: auto) {
height: $height;
width: $width;
background: $background;
left: $left;
right: $right;
top: $top;
bottom: $bottom;
}
Then use this mixin like this:
.door-sign {
@include paint(1.25vmin, 10vmin, $door-sign, right: 8.75vmin, top: 22vmin);
}
The paint mixin uses the height and width, background color, and positioning to create a paint element. To avoid a syntax error, I had to set the left, right, top, and bottom to auto.
At first, this approach seemed better, but as I went along, I found it too abstract and decided I liked having each element explicitly stated, especially since I had to constantly go back and refer to other parts of the painting. Normally, refactoring is always good (although too much can be bad) but in this case, I was more interested in the end result than writing clean code.
I ran into a few small issues with viewport lengths. First, the smaller the vmin, the less accurate it seemed to be. Try to avoid using numbers like 4.15vmin and instead try to stick close to quarter widths, like 4.25vmin, or 4.75vmin instead.
Second, occasionally when using vmin, the more precise measurements seemed to change slightly when the page was made larger or smaller. I’m not sure if this is browser specific or if it happens when smaller viewport lengths are used, but this seems to be related to the first issue I mentioned above. Overall though, these were the only issues I ran into.
This exercise gave me a broader understanding of CSS. After completing a few of these paintings, I’ve noticed that I approach CSS a little differently, taking a bit more of an artistic approach, but also really thinking about each piece on the page as its own module (instead of a whole page). This has made my CSS much better.
If you create something cool, leave a comment and let me know what you learned in the process.
As web frontends get increasingly complex, resource-greedy features demand more and more from the browser. If you’re interested in monitoring and tracking client-side CPU usage, memory usage, and more for all of your users in production, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording everything that happens in your web app, mobile app, or website. Instead of guessing why problems happen, you can aggregate and report on key frontend performance metrics, replay user sessions along with application state, log network requests, and automatically surface all errors.
Modernize how you debug web and mobile apps — start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowThe useReducer React Hook is a good alternative to tools like Redux, Recoil, or MobX.
Node.js v22.5.0 introduced a native SQLite module, which is is similar to what other JavaScript runtimes like Deno and Bun already have.
Understanding and supporting pinch, text, and browser zoom significantly enhances the user experience. Let’s explore a few ways to do so.
Playwright is a popular framework for automating and testing web applications across multiple browsers in JavaScript, Python, Java, and C#. […]