Sunday, January 5, 2014

CSS Rotated Text: Parent Dimensions and Spacing Issues

Rotating text with CSS seemed like an great solution to my space problem. I have a table of narrow columns but need labels that will not fit in that space. Rotating them 90 degrees would fix the problem. However, fixing one problem only exposed another issue. The following examples will use this block of HTML to define the content for the columns that need to be rotated.


  <ul class="list-unstyled">
      <li><span>A column label</span></li>
      <li><span>Short</span></li>
      <li><span>Much longer than the last</span></li>
      <li><span>Another label</span></li>
      <li><span>The final column</span></li>
  </ul>



Without any rotation, the markup renders like this:

  • A column label
  • Short
  • Much longer than the last
  • Another label
  • The final column


Now, let's try to apply a transform to rotate the text.

First Attempt



What doesn't work is simply rotating the children elements of the UL. The height/width of the parent will be calculated based on the unrotated children. The result is not desirable:

.list-rotated > li {
   display: inline-block;
}

.list-rotated > li > * {
   display: inline-block;
   white-space: nowrap;

   transform: translate(0,100%) rotate(-90deg);
   transform-origin: 0 0;
}


  • A column label
  • Short
  • Much longer than the last
  • Another label
  • The final column


You can see how the rotated text flows up into the prior content and the spacing between each child appears the be the same as the width prior to being rotated.

Fixing Height



Clearly, what seemed like a simple solution was not going to be as easy as I was hoping. At first, I punted and rotated the whole UL, fixed the height of the container, and tweaked the positioning until it fit correctly. Obviously, this was a fragile solution that would need to change every time the content changed. Feeling a little defeated, I did a little searching and found a novel solution using the :before pseudo-element to turn each rotated element into a square. Doing this ensures the element measures the same in both height and width making it easier to rotate and properly fill the parent. That handled forcing the container to size properly to match the widths of the rotated elements. However, the spacing between each element still needs to be fixed. That was dealt with by fixing the width and using overflow: hidden to collapse the space:


.list-rotated > li {
   overflow: hidden;
   display: inline-block;
   width: 1.5em;
   line-height: 1.5;
}

.list-rotated > li > * {
   display: inline-block;
   white-space: nowrap;

   transform: translate(0,100%) rotate(-90deg);
   transform-origin: 0 0;
}

.list-rotated > li > *:before {
   content: "";
   float: left;
   margin-top: 100%;
}


Now, you can see the content is rotated and properly fill the space without sliding up into the text above it and the spacing is consistent:

  • A column label
  • Short
  • Much longer than the last
  • Another label
  • The final column


This works a lot better than the original attempt. However, one thing that bothered me was the width on the wrapper element. Using it meant the content must not exceed that limit or it will be clipped. This becomes more apparent when you add some boxes and padding the the child elements:



Fixing Spacing



To solve the spacing issue, I borrowed an idea for justifying text to create consistent spacing between the child elements. So far, the UL container has not had any styling. The LI elements still need a width to make this work, but the spacing is no longer determined by the width. Instead, the parent container can control the width and the rotated children will evenly fill the space:


.list-rotated {
   text-align: justify;
}

.list-rotated:after {
   content: "";
   width: 100%;
   display: inline-block;
}

.list-rotated > li {
   display: inline-block;
   width: 0;
}

.list-rotated > li > * {
   display: inline-block;
   white-space: nowrap;

   transform: translate(0,100%) rotate(-90deg);
   transform-origin: 0 0;

   vertical-align: bottom;
}

.list-rotated > li > *:before {
   content: "";
   float: left;
   margin-top: 100%;
}




One Final Tweak



The above solution works well until it needs to line up with text that is not rotated. All those sizing fixes overrun the UL container and cause the justification calculations to be off:

  • A column label
  • Short
  • Much longer than the last
  • Another label
  • The final column
  • A
  • B
  • C
  • D
  • E


My final fix was to add a line-height: 0 on the LI elements and add any lost spacing back as padding on the UL:


.list-rotated {
   text-align: justify;
   padding: 0 1em;
}

.list-rotated:after {
   content: "";
   width: 100%;
   display: inline-block;
}

.list-rotated > li {
   display: inline-block;
   width: 0;
   line-height: 0;
}

.list-rotated > li > * {
   display: inline-block;
   white-space: nowrap;

   transform: translate(0,100%) rotate(-90deg);
   transform-origin: 0 0;

   vertical-align: bottom;
}

.list-rotated > li > *:before {
   content: "";
   float: left;
   margin-top: 100%;
}


  • A column label
  • Short
  • Much longer than the last
  • Another label
  • The final column
  • A
  • B
  • C
  • D
  • E


Now everything lines up as expected. While probably not perfect for every scenario, it is flexible enough for my needs. Various things like font-size and padding can easily upset the alignment between the rotated elements and the non-rotated content. However, this solution is a lot more flexible than what I originally started with.