How to layout rotated text in React Native

Rotating text is easy in React Native; simply apply a css transform. But css transforms get applied after layout happens, so how can we make sure our rotated text is where we want it to be?

Maarten Schumacher
5 min readDec 10, 2018

Recently I had to implement a design that looked like this:

For the rotated text label, I first tried naively applying a rotation, like so:

<Text style={{ transform: [{ rotate: “90deg” }] }}>{text}</Text>

Which resulted in:

The labels don’t line up with their bars, they don’t line up horizontally with each other, and the gaps between the labels are spaced unevenly. What happened?

I remembered that css transforms are applied after layout happens. So we need to imagine that after layout, the labels have a width that depends on the amount of text they need to hold, not knowing they will have more horizontal space once the labels have been rotated. After the rotation is applied, the labels don’t layout again to take advantage of the freed up space. They couldn’t even, because the layout system has no way of accounting for css transforms.

Instinctively I then tried to give the text labels a fixed width, so they shift to the left and have equal gaps between them:

<Text style={{ transform: [{ rotate: “90deg” }], width: 14 }}>
{text}
</Text>

Which resulted in:

Yikes. Now what? Again we have to imagine how the view would be laid out if the transform didn’t happen. When a text field has a small width, it by default breaks the line to continue the text on the next line.

To get around this, I applied a little trick I was pleasantly surprised actually works (and hope still works in future versions of React Native): I gave the text view a large width so the text doesn’t have a line break, but then I created a wrapper around that text view with a small width, so that the layout will be correct:

<View style={{ width: 14 }}>
<Text style={{ transform: [{ rotate: “90deg” }], width: 40 }}>
{text}
</Text>
</View>

Result:

Not quite there yet, but at least the gaps between the labels are even, and they line up horizontally. Note: on Android you wouldn’t see much at this stage because Android refuses to draw child views that aren’t within the bounds of their parent views.

So now two problems remain: it doesn’t work on Android, and the labels aren’t positioned correctly underneath the bars. It seems they are all somehow shifted up and to the right a bit. To explain this, we need to realise how css rotations work. Annoyingly, the origin point of rotation, the point around which the view is rotated, is in the center of the view. So again, imagine how the text is laid out before rotation happens. Then, imagine pushing a pin into the very center of the text, and then rotating it. It ends up being too far up and to the right of where we want it to be. On the web we could change the rotation origin by messing with transform-origin, but it’s not supported in React Native. Let’s add some background colours to easier visualise where things need to go:

<View style={{ width: 14, backgroundColor: "red" }}>
<Text style={{ transform: [{ rotate: “90deg” }], width: 40, backgroundColor: "yellow" }}>
{text}
</Text>
</View>

At this point we simply need to push the yellow view down and to the left to get it aligned with the red view. We’ll use translateX and translateY transforms instead of messing around with position: absolute, since we don’t want to complicate things by adding a third positioning system into the mix. At this point it’s tempting to just turn on hot reloading and tweak the numbers until there’s a fit, but that wouldn’t be very maintainable. If another developer came along and needed to slightly increase the height of the label, because of a copy change for example, they probably wouldn’t even realise these numbers needed to be re-tweaked, and the layout would become slightly off. So we’ll need to figure out the correct offset using the height and length of the text label:

const TEXT_LENGTH = 40
const TEXT_HEIGHT = 14
const OFFSET = TEXT_LENGTH / 2 - TEXT_HEIGHT / 2
<View style={{ width: TEXT_HEIGHT, height: TEXT_LENGTH }}>
<Text style={{
transform: [
{ rotate: "90deg" },
{ translateX: -OFFSET },
{ translateY: OFFSET }
],
width: TEXT_LENGTH,
height: TEXT_HEIGHT
}}>
{text}
</Text>
</View>

That’s better. Thankfully our Android problem is also solved, since the child content is now drawn within the bounds of the parent view (even though according to the layout system it’s still clipped). Thanks Android!

Remove those background colours and it’s done. Note: I realise I’m using a fixed height and width for the text view, which wouldn’t be possible if the text were dynamic, or there were different translations. In that case, you could get the laid out text width and height using onLayout.

Didn’t work for you? Know a cleaner way to do it? Let me know in the comments!

--

--

Maarten Schumacher

React Native developer, functional programming enthusiast