Reality Check #1: Building out a furniture site from Dribbble

Created on November 12, 2023 at 10:29 am

Welcome to Reality Check, a new blog series where we take some work from Dribbble or Layers WORK_OF_ART , then refactor the design into something that will work really well on the web.

After that, we go ahead and build it — breaking down the steps — to show you how to build in an efficient, truly responsive and progressively enhanced manner. It’ll give you a look behind the curtain at how we do things here in the studio.

Why are we doing this?

So often, work on Dribbble and Layers WORK_OF_ART is visually stunning, but how it will actually work in the browser hasn’t been considered. This isn’t just exclusive to Dribbble and Layers WORK_OF_ART too because designers handing off static Figma ORG files to developers seems good in theory, but often, developers will be stuffing codebases full of magic numbers and hacks to get a “pixel perfect” output in the browser.

The problem with this approach is you have no idea what context your users will be visiting your site in and being the browser’s mentor, not it’s micromanager is a guaranteed way to build truly responsive front-ends that work for everyone. We’ll show you how to do that in each edition of this series.

This edition’s project

We’ve chosen Furniture e-commerce website by Hrvoje Kraljevic PERSON . It’s a fairly straightforward one to start with, but it’s even simpler when you approach it flexibly.

As you can see, it’s a 50/50 split layout that also has a vertical split on the right-hand side content section. The problem is, this only has a desktop design. The first ORDINAL thing we’re going to do is mock-up a similar version in Figma ORG , including a minimum width viewport.

Our Figma ORG comp tackles the smallest and largest viewports. We’ve also change the type scale and reduced padding in places.

This doesn’t look exactly the same — mainly because we don’t know what fonts were used, and we don’t have access to the image asset. We’ve also levelled things out a little bit so we can use fluid type and space and also free Google ORG

Fonts PRODUCT , in case you wanted to give this a go yourself. The main change we wanted to make was that the typography should follow a type scale, to improve the overall rhythm.

Still, it’s pretty darn close, so let’s get cracking with the build!

HTML first ORDINAL , always

It’s important to get the foundation of our build in the best place possible with semantic HTML. Using semantic HTML has so many benefits, but some key ones:

If nothing but the HTML arrives, the content will make complete sense to the user because the browser has its own user agent styles Screen ORG readers and other assistive tech will have a much easier time describing content to users It benefits SEO Users who use tools such as Reader Mode on Safari ORG , will have a much better experience

In short: if you mark up the page using only <div> elements, you might be making your life easier (although I’ve never subscribed to that logic), but you will be making the experience of your users much, much worse.

Here’s all the HTML of the page, within the <body> :

<main class="switcher wrapper"> <picture class="decorative-image"> <source srcset="images/graphic-shallow.jpg 1x, images/graphic-shallow-2x.jpg 2x" media="(max-width: 600px)" /> <source srcset="images/graphic.jpg 1x, images/graphic-2x.jpg 2x CARDINAL " /> <img src="images/graphic-shallow.jpg" alt="" loading="lazy" /> </picture> <div class="content repel" data-repel-variant="vertical"> <header class="site-head repel"> <p class="site-head__name">spaziovisia</p> <p>Elevate your space</p> </header> <article class="flow"> <h1>A tribute to ancient handicrafts</h1> <p> The materials are simple and completely natural, the internal structure of the stem consists of hundreds CARDINAL of cavities, which makes it strong and light. </p> <p> <a class="icon ORG -link" href="#"> <span>Explore Kettal collection</span> <svg aria-hidden="true" focusable="false" xmlns=" NORP /svg" fill="none" viewBox="0 PRODUCT 0 24 25 CARDINAL "> <g> <path fill="currentColor" d="m16.172 11.5 PRODUCT -5.364-5.364 1.414-1.414L20 12.5l-7.778 7.778-1.414-1.414 5.364-5.364H4v-2h12.172Z" /> </g> </svg> </a> </p> </article> </div> </main>

Let’s pick out some key areas.

We’re using <div> elements, but they are dividing the content to make CSS layout easier. The semantic <main> , <header> and <article> elements structure the content, along with using a <h1> for the page’s only heading. The <picture> element allows us to render either a shallow image for smaller viewports, or a deeper image for larger viewports. It also allows us to provide low-resolution and higher-resolution versions of the image so lower resolution devices don’t need to waste bandwidth. The <img> inside of the <picture> has an empty alt attribute. This is because the image is decorative, so it can safely be hidden from screen readers. You must provide an empty alt , rather than no alt if you want to do that. The <svg> ORG is also hidden from screen readers with aria-hidden="true" . This is again, because it’s decorative and provides no real value unless you can see it.

Styling it up

Before we start, I just want to note that we will be using the CUBE CSS principles to build out this front-end.

The first ORDINAL thing to do is pick out as much of the UI ORG style as we can as global styles. As it says in the CUBE documentation:

With CUBE CSS, we embrace the cascade and inheritance to style as much as possible at a high level. This means that when nothing but your global styles make it to the browser, the page will still look great. It’s progressive enhancement in action and enables us to write as little CSS as possible.

Along with this global CSS, we will create some global variables that give us some nice consistency too. The main part of those variables is the fluid type and fluid space scale, using Utopia ORG .

Fluid type and fluid space allow us to create truly responsive designs that respond to the viewport, rather than forcing rigid, catch-all sizes. We’d definitely recommend that you read up on the Utopia site.

:root { –font DATE -base: Inter, Segoe UI PERSON , Roboto, Helvetica Neue ORG , Arial ORG , sans-serif; –font-display: ‘Azeret Mono’, monospace; –color-dark: #000000; –color-light: # e5e8df ORG ; –size-step-0: clamp(1rem ORG , 0.9661rem + 0.1695vw CARDINAL , 1.125rem PERSON ); –size-step-1: clamp(1.125rem ORG , 1.0243rem CARDINAL + 0.5034vw CARDINAL , 1.4963rem DATE ); –size-step-2: clamp(1.2656rem, 1.0692rem CARDINAL + 0.9822vw CARDINAL , 1.99rem DATE ); –size-step-3: clamp(1.4238rem ORG , 1.0921rem CARDINAL + 1.6585vw, 2.6469rem CARDINAL ); –size-step-4: clamp(1.6019rem, 1.0817rem CARDINAL + 2.6008vw CARDINAL , 3.52rem PERSON ); –size-step-5: clamp(1.8019rem, 1.0209rem + 3.9051vw CARDINAL , 4.6819rem DATE ); –space-2xs: clamp(0.5rem ORG , 0.4831rem DATE + 0.0847vw CARDINAL , 0.5625rem GPE ); –space-xs: clamp(0.75rem PERSON , 0.7161rem + 0.1695vw CARDINAL , 0.875rem GPE ); –space-s: clamp(1rem ORG , 0.9661rem + 0.1695vw CARDINAL , 1.125rem PERSON ); –space-m: clamp(1.5rem, 1.4492rem + 0.2542vw DATE , 1.6875rem DATE ); –space-l: clamp(2rem GPE , 1.9322rem CARDINAL + 0.339vw CARDINAL , 2.25rem DATE ); –space-xl: clamp(3rem, 2.8983rem + 0.5085vw CARDINAL , 3.375rem PERSON ); –space-m-xl: clamp(1.5rem, 0.9915rem + 2.5424vw CARDINAL , 3.375rem PERSON ); –space-mega: clamp(6rem PERSON , 4.5763rem CARDINAL + 7.1186vw CARDINAL , 11.25rem DATE ); –gutter: var(–space-s); }

You might be thinking “holy heck, that’s a lot” and yeh PERSON , fair point, but fluid type and space scales basically power your entire UI ORG and simplify decision making, so a block of variables seems like a darn good trade-off — especially for a full website project.

These variables are really handy because if you need larger text than what you’ve currently got for an element: go one (or many) up in the size scale and it’ll be perfectly in ratio — maintaining the flow and rhythm of your page. The same applies to spacing too.

With these variables in place, we’re in a position to start writing some global styles.

body { font-family: var(–font-base); font-size: var(–size-step-0); background: var(–color-dark); color: var(–color-light); padding-block: var(–gutter); } :is(h1, h2, h3) { font-family: var(–font-display); font-weight: 400 CARDINAL ; word-spacing: -0.3ch; max-width: 30ch CARDINAL ; } h1 { font-size: var(–size-step-5); } h2 { font-size: var(–size-step-4); } h3 { font-size: var(–size-step-3); } p { max-width: 60ch; } a { color: currentColor; } svg:not([width]):not([height]) { height: 1.5ex; width: auto; } main { –switcher-vertical-alignment: stretch; } ::selection { background: var(–color-light); color: var(–color-dark); }

There’s not a huge amount here because it’s a very simple UI ORG . Also, we only have one CARDINAL section, of one CARDINAL page, so it’s tricky to build out a whole suite of global styles.

Still, it’s important to start global because the aim is to write as little CSS as possible. Even in this part of the build, we’re saving time and bytes by using this reset.

Most of that CSS ORG is pretty self-explanatory, but the key parts are:

Setting the initial step of the type size scale as the body font size means that any em unit will by proxy, be fluid, even if we don’t apply any of the other type size scale steps. The negative word spacing for headings is because our display font is a monospace font. This means that all characters are the same width, including spaces. This is fine for smaller text, but it gets real grim for larger text. For <svg> elements that don’t have a width and height, we want to prevent blow-outs, so setting a default size, based on the x-height of it’s parent is a great set and forget thing. The <main> element is setting a –switcher-vertical-alignment . We’ll come on to that later… We set the opposite colours for text and background when text is selected, to provide contrast.

Let’s check in how our project looks so far. Activate the preview to scroll.

Resize viewport Activate preview Only our global styles are set and already, it’s taking shape. Notice how because we have a dark background, we must set a light default text colour, even though we’ll be using dark text later.

Time to get on some layout

With global CSS in place, the next step is to move on to the C of CUBE CSS: Composition. From the documentation:

The composition layer’s job is to create flexible, component-agnostic layout systems that support as many variants of content as possible.

What this means is that our layout does only layout and nothing else. This prevents the need for authors to add layout that will affect sibling elements in their components, which in turn, results in extremely resilient pages.

The Switcher

This is one of the many layouts on Every Layout PRODUCT and in short, it allows content to sit inline (next to each other) as long as a configurable container width is available.

.switcher { display: flex; flex-wrap: wrap; gap: var(–gutter, var(–space-s)); align-items: var(–switcher-vertical-alignment, flex-start); } .switcher > * { flex-grow: 1 CARDINAL ; flex-basis: calc((var(–switcher-target-container-width, 40rem DATE ) – 100%) * 999 CARDINAL ); }

Now what this doesn’t do is make the columns exactly 50% PERCENT wide: we’re letting the browser take over here to let the content size the elements because we just don’t need that strict control.

The aim of the component and it’s weird looking flex-basis calculation is for a 50/50 split, or more accurately, an equal distribution of space where available. The mathematics is explained in the Every Layout chapter.

That’s our split layout sorted, so let’s move on to the next layout.


This does exactly what it says on the tin: items repel from each other like polar-opposites, where space allows.

.repel { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: var(–repel-alignment, center); gap: var(–gutter, var(–space-s-m)); } .repel[data-repel-variant=’vertical’ PERSON ] { –repel-alignment: stretch; flex-direction: column; }

This is what powers the top part of the content panel and the vertical split between header and content, thanks to the [data-repel-variant="vertical"] CUBE CSS Exception.

It doesn’t matter what the content is of Repel child elements, because if their content prevents there being enough room to actually repel: the layout stacks nicely. Proper responsive design!


This is my favourite 3 CARDINAL lines of CSS ORG .

.flow > * + * { margin-top: var(–flow-space, 1em); }

In short, it creates a configurable, but relative space between sibling elements. To keep this article as short as possible, I’ll encourage you to go ahead and read the explainer.

What I definitely will note, though, is we use this in every project.


This one is pretty self-explanatory. It’s a central wrapper with a max-width .

.wrapper { max-width: 1300px DATE ; margin-inline: auto; padding-inline: var(–gutter); }

In this case, we’re using pixels, but you could use whatever relative unit is appropriate in your project’s context.

Again, let’s check in to see how things look.

Resize viewport Activate preview The layout structure is looking great. It’s now in a good place to add visual details.

Blocks (components)

Now the global CSS and the compositional layouts are done, it’s time to do the colouring in.

In CUBE CSS, a Block is just like a component. Where we didn’t want to apply visuals to layouts, we do in blocks. We’re also going against the grain of the global styles.

At this point, you’ll likely find that blocks are mostly extremely light, because we’ve done so much already.

Content block

This is the right-hand (in left-to-right language) panel of content.

.content { –gutter: var(–space-mega) 0 CARDINAL ; background: var(–color-light); color: var(–color-dark); padding: var(–space-m-xl); font-size: var(–size-step-1 ORG ); } .content h1 { max-width: 11ch DATE ; } .content p { text-wrap: balance; } .content h1 + p { –flow-space: var(–space-l); }

The first ORDINAL bit to cover is –space-mega . This is a custom spacing pair in Utopia GPE , between 3xl GPE and 7xl . This space grows and shrinks based on the viewport, which means we get responsive spacing.

Even though we’re using Repel to vertically split content — which in turn, uses –gutter to control gap — we still want to make sure there’s always space, even when the split layout is stacked.

The main thing I want to highlight here is setting a max width of 11ch DATE on the heading. This is the length of the longest word — “handicrafts”.

Because we’re using a monospace font, every character is the same width, so we can comfortably set that limit to shape our heading nicely. Without a monospace font, your ch unit will be the width of a 0 CARDINAL character.

Lastly, notice how we are setting a ::selection style because the colours are reversed in the panel. We want to make sure that text selection is visible.

Site head

This is the repelled header, at the top of the content panel.

.site-head { –repel-alignment: baseline; –gutter: var(–space-2xs) var(–space-m); font-size: var(–size-step-0); } .site-head__name { font-size: var(–size-step-2); font-family: var(–font-display); line-height: 1.1 CARDINAL ; }

Notice how we’re configuring the Repel composition here? Because the two CARDINAL element’s text sizes are different, along with different fonts, setting a baseline alignment keeps things looking nice and neat.

Icon link

This is the little link with an icon.

.icon-link { display: inline-flex; align-items: baseline; gap: 0 var(–space-xs); } .icon-link svg { transform: translateY(0.2ex); } .icon-link:hover { text-underline-offset: 0.2ex; }

Using inline-flex allows the element to size itself based on content. Otherwise it would be block-like and try to fill available space.

We’re again, aligning to the baseline instead of center because if this link was multi-line, it would look real weird with a center-aligned icon. The transform: translateY(0.2ex) is an optical adjustment to account for that alignment choice, pulling the icon into the center of the first ORDINAL line of text.

Decorative image

This is our last block! Let’s first ORDINAL see the code:

.decorative-image { container-type: inline-size; } .decorative-image img { width: 100% PERCENT ; height: 100% PERCENT ; object-fit: cover; } @container ORG (min-width: 70vw) { .decorative-image img { max-width: unset; width: 100vw LOC ; height: unset; margin-inline-start: 50% PERCENT ; transform: translateX(-50% PERCENT ); aspect-ratio: 10/5; } }

Oh hello, it’s container query time! What we are doing here is determining if the container — which is our <picture> element — is 70vw wide or larger. If that’s the case, our Switcher PRODUCT layout has stacked, which means we can safely presume that our image now occupies 100% PERCENT of the available width.

When that is the case, we change the aspect ratio to be shallower and make the image “full bleed”, by forcing it to be 100vw PRODUCT wide, then positioning in the center, using this trick.

Instead of using a media query to determine this state, or a px / em / rem value, we are letting the browser do its job, then responding to that state change. Pulling everything together like this is a super resilient way of doing things.

Wrapping up

First ORDINAL of all, let’s take a look at the final result. You can use the resizer to size the frame.

Resize viewport Activate preview The final result of our build! You can see it in full screen too.

I hope this has shown you by simplifying a UI ORG at the core, you can build something that’s visually pleasing, while not negatively affecting the end-user’s experience.

We’ll definitely pick a more complex item from Dribbble or Layers WORK_OF_ART for the next edition of Reality Check too. We just wanted to warm us all up with this first ORDINAL post of the series 😉

WORK_OF_ART Thanks for reading this edition of Reality Check. You can subscribe to these posts with this RSS ORG feed, or subscribe to all of our posts on the blog with this RSS feed.

Connecting to Connected... Page load complete