github.com/angrybunnyman.com

Portrait of the Man as a...

I lost my theme's spine so brought it back. All hail CSS rotation.

Portrait of a Spinal Column

Post Nutrition Label

  • Content Type: CSS
  • Read Time: 5 min
  • Topics: Design
  • Tone: Nerdy

The Spine: On Rotating a Header Until It Became Architecture

At some point the header stopped being navigation. It stopped being a polite horizontal bar asking to be clicked. It became a structural element. A margin with authority. A binding.

I wanted the site to feel less like a webpage and more like a book cracked open along its left edge. Something that contains the writing instead of hovering above it.

So I rotated the header. And then everything got weird.

Rotation Is a Lie (Until You Understand It)

The header is rotated:

transform: rotate(-90deg);

That seems harmless. It is not harmless. When you rotate an element, CSS does not "swap" width and height in a friendly way. It simply rotates the box in space. Your mental model must rotate with it.

After a -90deg rotation:

  CSS property   What it becomes visually
  -------------- --------------------------
  `width`        Vertical length of spine
  `height`       Thickness of spine

That means this line:

height: var(--spine-gutter);

...does not control height in the way your brain wants it to. It controls how thick the left margin appears. You are no longer sizing a header. You are carving a column into the viewport.

The Math That Prevents the Spine From Eating Itself

The spine contains two lanes:

That means the thickness must account for:

Here's the calculation:

--spine-lane: clamp(2.6rem, 5.8vw, 3.9rem);
--spine-row-gap: .40rem;
--spine-pad-y: .45rem;

--spine-gutter: calc(
  (2 * var(--spine-lane))
  + var(--spine-row-gap)
  + (2 * var(--spine-pad-y))
  + .25rem
);

Centering a Rotated Thing Without Summoning Chaos

The stable version is almost boring:

header{
  position: fixed;
  left: 0;
  top: 50%;

  transform-origin: left center;
  transform:
    translateY(-50%)
    translateX(var(--spine-shove))
    rotate(-90deg);
}

Three rules:

  1. top: 50% + translateY(-50%) centers vertically.
  2. translateX(var(--spine-shove)) pushes the rotated element into view by a fixed amount.
  3. No percentage offsets tied to viewport geometry.

The result:

The spine becomes anchored. Not drifting. Not haunted.

The Background Problem: Floating vs Structural

Originally the header had its own background. That made it look like a floating strip on top of the grid background, like a sticker.

That was wrong.

The fix was architectural:

body::before{
  content:"";
  position: fixed;
  top: 0;
  left: 0;
  height: 100vh;
  width: var(--spine-gutter);

  background: var(--block-background);
  border-right: var(--rule-heavy);
}

Now the spine is:

The content does not overlap it. It grows beside it.

Orientation, Safari, and the Betrayal of vh

On iPad, 100vh does not always mean "the visible screen." Safari's dynamic chrome shifts it.

So the spine constrains itself like this:

@supports (height: 100dvh){
  header{
    max-width: calc(100dvh - (2 * var(--spine-inset)));
  }
}

dvh = dynamic viewport height.

Which means when the browser UI moves, the math updates.

The Formula Generalizes

If I ever want three rows:

--spine-gutter =
  (3 * lane-height)
  + (2 * row-gap)
  + (2 * padding)
  + safety

It scales.

Why I Like This

The spine is not decorative. It is a constraint with backbone...

Reply on Bluesky   Reply by Email (or just say hi!) Reply on Mastodon  

Footnotes

#CSS