Design
Responsifying with Viewport Units
Not long ago, I designed what seemed like a tricky heading to implement. For our upcoming Ancient City Ruby conference, I placed two lines of text on either side of “450” (the number of years St. Augustine has been a city), taking up the full width of the containing column. Oh, yeah, and I wanted this heading to be responsive.
CSS all the way
If you’re not using Javascript to determine browser width — which I wasn’t, for the sake of finding a CSS-only solution — you might think you could use percentages or rems to make this work. But those solutions don’t cause the text to resize correctly, because percentages and rems are relative to their parents and can’t calculate the size of the browser window.
Viewport units are nice because they let us create elements relative to the viewport instead of a container. That’s why, within a container that has a max-width
, you can still set elements to take up space according to the viewport size, something you can’t do with percentages or rems.
This layout was a good test case for viewport units, which I’d heard of but never used before. Viewport units — vw
(viewport width), vh
(viewport height), vmax
(the larger of the two), and vmin
(the smaller of the two) — represent 1% of the viewport size, and are fully supported by the big three (Firefox, Chrome, and Safari). IE 9+ has partial support (because it uses vm
instead of vmin
), and all modern mobile browsers except Opera support viewport units. There’s some bugginess in iOS 7 related to vh
, but there are workarounds if you need them. We won’t be using vh
in this example, though.
Let’s get started
First I created a containing element called .column
, with a span
for each of the elements inside:
.column
%span.opening St. Augustine celebrates
%span.number 450
%span.closing years of history in 2015
I positioned .column
relatively, with a width
of 80% and max-width
of 1000px. (Again, this wrapper won’t affect child elements that have viewport units. This is because viewport units remain relative to the viewport instead of parent containers.)
.column
text-align: center
position: relative
width: 80%
max-width: 1000px
I absolutely positioned .first
and .last
on the left and right edges of .column
and added a transition effect to smooth resizing motion.
span
transition: .5s
&.opening
left: 0
&.closing
right: 0
&.opening, &.closing
position: absolute
I styled each of the span
elements, and added a tiny bit of left margin to .number
because it isn’t actually dead center. “St. Augustine celebrates,” the preceding span
, is a bit longer than the last, “years of history in 2015,” so .number
needed to be nudged a bit.
span
transition: .5s
&.opening
left: 0
&.closing
right: 0
&.opening, &.closing
border-top: 1px solid $red
border-bottom: 1px solid $red
display: inline-block
position: absolute
top: 28%
padding: .8rem
&.number
color: $red
font-weight: 700
letter-spacing: -2px
margin: 0 0 0 3%
Add some magic
Finally, I implemented vw
units for the text, with rem
sizes for fallbacks (looking at you, IE 8). The font size is determined by the current viewport width, so as you resize your browser, the letters fill the amount of space specified — and at most sizes, this works really well.
span
transition: .5s
&.opening
left: 0
&.closing
right: 0
&.opening, &.closing
font-size: 1.3rem
font-size: 2vw
border-top: 1px solid $red
border-bottom: 1px solid $red
display: inline-block
position: absolute
top: 28%
padding: .8rem
&.number
color: $red
font-size: 3rem
font-size: 8.5vw
font-weight: 700
letter-spacing: -2px
text-align: center
margin: 0 0 0 3%
Up and down
Of course, I wanted this layout to work at different breakpoints. I wrote rules to create a pixel-based font size when the viewport reaches the largest comfortable width, because I didn’t want the text to grow to infinity, and since the wrapper has a max-width
the text could eventually outgrow it. To account for this, I set the font size for .open
and .close
to 24px and .number
to 129px beyond a browser width of 1300px.
When sizing down, there wasn’t a whole lot to change. I dragged my browser window back and forth to eyeball top margins for .opening
and .closing
, which I changed at different breakpoints to keep the content vertically centered on either side of .number
.
I set rules at max-widths of 480px and 680px to keep the text at a smaller viewport width on mobile devices. For viewports under 680px, the text stacks for readability instead of centering.
span
transition: .5s
&.opening
left: 0
&.closing
right: 0
&.opening, &.closing
font-size: 1.3rem
font-size: 2vw
border-top: 1px solid $red
border-bottom: 1px solid $red
display: inline-block
position: absolute
top: 28%
padding: .8rem
+min-width(1300)
font-size: 24px
top: 32%
+max-width(840)
top: 24%
+max-width(700)
top: 18%
+max-width(680)
font-size: 1.8rem
font-size: 4.3vw
display: block
position: relative
top: 0
&.number
color: $red
font-size: 3rem
font-size: 8.5vw
font-weight: 700
letter-spacing: -2px
text-align: center
margin: 0 0 0 3%
+min-width(1500)
font-size: 129px
+max-width(480)
font-size: 4rem
font-size: 12vw
For those of you following along at home, below are the mixins I’m referencing to set my breakpoints:
= min-width($width: 900)
@media screen and (min-width: #{$width}px)
@content
transition: all .5s ease-in-out
= max-width($width: 900)
@media screen and (max-width: #{$width}px)
@content
transition: all .5s ease-in-out
Done!
I was pretty happy with how the final heading turned out. (The live version is no longer available, but try it for yourself!)
You can use viewport units on anything, not just text. For my purposes, I found viewport units to be really helpful with font-size, and knowing it’s pretty cross-browser friendly is a compelling reason to use it in the future. Try implementing viewport units in your next project and see where it takes you.