Yes We Can Do Fraction Of A PixelSeptember 22, 2013

I was reading HN the other day and stumbled to Cloning the UI of iOS 7 with HTML, CSS and JavaScript. In that the author had problem of creating 1px solid border for retina screens.

Besides, another problem to be dealt with are Retina screens. On Retina iPhones, the browser automatically “translates” the 320x480px viewport into the 640x960px screen resolution. While this solution works well for most elements, it fails to reproduce the famous 1px-thick borders iOS uses extensively as they are translated to 2px-thick borders. Intuitively, we would be tempted to set a 0.5px border but this is impossible as it must be a positive integer. Yet, as always, there’s a solution, found by Stephan Bönnemann (see also this link by Brad Birdsall). Basically, it consists of replacing this code: border-bottom:1px solid rgb(200, 199, 204); by this one: box-shadow: 0 1px 1px -1px rgba(200, 199, 204, .5);.

Hmm. You can’t simply specify a border of 0.5px? Is that the consensus around front-end developers? Or can you?

Well, of course you can. That’s what i have been doing last 6 months - ditching the pixel based rendering.

CSS Transformations Can

It seems to be little known fact that with CSS transform you can get rid of pixels altogether! I’ll start with basics. Follow me.

Positioning

With CSS translate you can specify coordinates in fraction of pixels. Look at the example below.

See? No? Zoom in. This is no image, this is CSS and HTML. Outer box has outline specified, inside is ten 50×50px boxes. Each shifted down by 0.1px of left neighbour.

Width & Height

Ok, that was nice, but how we should deal with widths and heights? One can not translate them? Well, sure one can. Just specify any (really any) width/height as usual and scale the contents into size you exactly want. The example left has the same ten boxes, initially at size of 1×15px, first has real, scaled width of 50px, the last one 51px.

Easy? Not so fast. Now we have 2 problems to overcome:

<div style="
    width: 1px;
    height: 50px;
    transform: scaleX(50);
">
    <p>Holy-shmoly that's one UGLY stretched text
    <p style="
        /* 1/previous scale */ 
        transform: scaleX(0.02)
    ">Huh, back to normal :-)
</div>

And borders, how about them?

Armed with previous knowledge, lets draw those same ten boxes, with expected final size of 50×50px and border in steps from 1…2px. Sure you can you do that now?

We can specify any width, height, remember? So, for this i specify border in CSS in full pixels from 10…20px. Companioning that with width and height of 500px both. And then scaling all back to 50px. Here i encounter that aforementioned problem of original size, as i float those boxes. I cure this issue with outer box of 50×50px

<div style="
    width: 50px;
    height: 50px;
">
    <div style="
        width: 500px;
        height: 500px;
        translate: scale(0.1)
    ">
    </div>
</div>

To continue, we need some theory first.

How Transforms Transform

Transforms seems to me a bit afterthought and they do not work as other CSS layout properties. To illustrate that, i will construct translate emulation in canonical CSS. We will have outer #container and inside #rectangle.

<div id=container style="
    width: 400px; 
    height: 200px; 
">
    <div id=rectangle style="
        width: 100px; 
        height: 50px; 
        top: 10px;
        left: 10px; 
        margin: 10px
"></div>
    <p id=text>Hello</p>
</div>

I canonical CSS top, left, margin (and % units) are relative to that elements offsetParent - in this setup #rectangle's top/left corner is accordingly 20px left and 20px down from #containers top/left corner and if we run getBoundingClientRect() for #text, we see that top is 80 and left is 0. What happens now if we apply translate(-20px, -20px) to #rectangle? Where will #translate be, is obvious. Its top/left corner will now be at the same point with #cointainer ones. #rectangle.getBoundingClientRect() confirms that. Take notice - getBoundingClientRect() is always precice, it always returns real results, no matter what. But what about #text? Logically it should float 20px up - but it doesn’t. Why? Because the transforms will not change bounding box created using canonical CSS.

Virtually this happens when using transforms, if we want to emulate the translate above in canonical CSS:

<div id=container style="
    width: 400px; 
    height: 200px; 
">
    <div style="
        width: 100px;
        height: 50px; 
        top: 10px; 
        left: 10px; 
        margin: 10px; 
        visibility: hidden
    ">
        <div id=rectangle style="
            width: 100%;
            height: 100%; 
            top: -20px; 
            left: -20px; 
            margin: 0px; 
            visibility: visible
        ">
        </div>
    </div>
    <p id=text>Hello</p>
</div>

You now see that all transforms is applied not relative to offsetParent, but relative to “shadow” of itself. That’s the reason why #text above does not change place, no matter what transform we apply to the #rectangle. And thats why you can not float scaled and unscaled content of unknown dimensions together. Duh.

For example this

<div id=rectangle style=
    "width: 200px; 
    height: 100px; 
    translate: scale(0.5)
"></div>

produces box that reserves “room” in layout by 200×100px, but has rendered size of 100×50px.

Where, according to the layout rectangle, the displayed rectangle is positioned, depends from transform-origin property. In canonical CSS we have no origin variance. The origin is always 0 0 or top,left. Using transforms we can put origin whenever we want. CSS transform-origin default is 50% 50% or center center. By definition, origin is the point at which transform is applied. On translate, the origin does not matter, but it does matter on all other transforms. On simple rotate and simple scale - origin is the point that does not move. Continuing from previous, if we want to float #rectengle into other content, we need to wrap it into other DIV with correct dimensions and set origin to 0 0, so that #rectangle's top/left corner is kept in place.

<div id=wrap style="
    width: 100px; 
    height: 50px;
">
    <div id=rectangle style="
        width: 200px; 
        height: 100px; 
        translate: scale(0.5); 
        transform-origin: 0 0;
    "></div>
</div>  

Only content transforms. In the explanation of how transform is applied i wrote ‘margin settings are removed from copy’ and that is another weirdness of sort one needs to take into a ccount on scaling. Take this and guess what happens:

<div id=wrap style="
    width:100px; 
    height: 50px;">
    <div id=rectangle style="
        width:200px; 
        height: 100px; 
        translate: scale(0.5); 
        transform-origin: 0 0; 
        margin: 10px; 
        padding: 20px;
    ">
        Hello
    </div>
</div>  

In result, the margin from #rectangle to #wrap is still 10px, unscaled, but the padding is scaled down to 10px.

In the Real World

Let’s make some hairlines

I prefer border scaling.

.transform-border-hairline {
    border-bottom: 1px #ff0000 solid;
    transform: scaleY(0.5);
}

But backgrond scaling works too, albeit i did encountered that on some occasions background scaling gives us not so sharp line.

.transform-hairline {
    height: 1px;
    background: #ff0000;
    transform: scaleY(0.5);
}

Perhaps you have noticed something in this blog design already? Yes, all lines are hairlines!

Solving Brad Birdalls Button Issue

And finally i show how to properly style a nice button with 1px border and solve Brad’s problem in full and once for all.

So in order to create a higher resolution experience we need a way to set less than 1px borders.

Original, inital CSS

button {
  font-size: 13px;
  line-height: 36px;
  font-family: sans-serif;
  font-weight: bold;
  color: #fff;
  padding: 0 15px;
  border: solid 1px;
  border-color: #2978B0 #266FA5 #1C557D;
  border-radius: 3px;
  background: linear-gradient(to bottom, #50A9E7, #307CB3);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.2),
              0 1px 1px rgba(0,0,0,.08);
  text-shadow: 0 -1px 0 rgba(0,0,0,.25);
}

produces this:

Follow @bradbirdsall

With CSS Transforms applied

.button-with-half-pixel-border {
    font-size: 26px;
    line-height: 74px;
    font-family: sans-serif;
    font-weight: bold;
    color: #fff;
    padding: 0 30px;
    border: solid 1px;
    border-color: #2978B0 #266FA5 #1C557D;
    border-radius: 6px;
    background: linear-gradient(to bottom, #50A9E7, #307CB3);
    box-shadow: inset 0 2px 0 rgba(255,255,255,.2),
              0 2px 2px rgba(0,0,0,.08);
    text-shadow: 0 -2px 0 rgba(0,0,0,.25);
    transform: scale(0.5);
}

we can achieve this:

Follow @bradbirdsall

As you see, I have doubled all values except border width. To compensate that, i did set line-height 2px bigger than 2×original.

Happy scaling!