center-ish

50 slightly incorrect, hilarious, or infuriating ways
to vertically center content

centered?

A field guide to centering vertically. The way it's done in the field. By people who are angry. Each technique is its own page. The dotted line on the preview shows where center actually is.

Technically correct, theatrically wrong

01 just a <table> (federally mandated) works

Compliant with the Department of Internet Things' IE5-compatibility mandate. We support exactly one (1) browser. It was released in 1999. We are not allowed to upgrade it.

<table border="2" cellpadding="20" cellspacing="0"
       width="100%" height="100%" bgcolor="#faf6e8">
  <tr><td valign="middle" align="center">
    am I centered?
  </td></tr>
</table>
preview →

02 Grid place-items on a 50-row template works

Why use one row when you can use fifty and put the content in row 25, give or take.

display: grid;
grid-template-rows: repeat(50, 1fr);
place-items: center;
/* good luck explaining this in code review */
preview →

03 works until a parent has position: relative footgun

With no positioned ancestor, an absolute + translate(-50%, -50%) child is positioned against the viewport — which centers it correctly! Until someone, anywhere up the tree, types position: relative. The preview has a button so you can watch it happen.

.parent { /* position: static — for now */ }
#centerish {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
}
/* one day .parent will gain position: relative
   for a tooltip, an overlay, anything. that day,
   the centering moves. nobody will know why. */
preview →

04 align-items vs align-self, three rounds each works

The container picks an alignment three times. The child overrides three times. The last declaration on each side wins. Centered, eventually.

body {
  display: flex;
  align-items: flex-start;     /* go top */
  align-items: flex-end;       /* no, bottom */
  align-items: center;         /* fine, center */
  justify-content: center;
}
#centerish {
  align-self: flex-start;      /* the top, please */
  align-self: stretch;         /* fill the whole thing */
  align-self: auto;            /* whatever the parent said */
}
preview →

05 line-height = 100vh (centered when terse) wrong

One line of text inside a line box that's 100vh tall ends up vertically centered. Add a second line and each one is 100vh tall. The page becomes a stairway.

html, body { height: 100vh; }
body {
  text-align: center;
  line-height: 100vh;
}
/* one line: centered. two lines: 200vh of content. */
preview →

06 table-cell wrapped in four divs works

Vertical-align: middle still works in 2026. We just have to dress it up.

<div><div><div><div style="display:table-cell;
  vertical-align:middle; height:100vh">
  ...
</div></div></div></div>
preview →

07 position: fixed; inset: 0; margin: auto works

Centered. Also pinned. Also covers whatever's in the middle of the page. Also doesn't scroll, ever. Also a cry for help.

#centerish {
  position: fixed;
  inset: 0;
  margin: auto;
  width: 300px; height: 80px;
}
preview →

08 aspect-ratio: 1 with place-content wrong

Centered when the window is square. Otherwise it's just sitting there.

body {
  aspect-ratio: 1;
  display: grid;
  place-content: center;
}
preview →

Off-by-half

09 top: 50% (and nothing else) wrong

Centers the top edge. The content sags below. Beautiful.

#centerish {
  position: absolute;
  top: 50%;
  /* I'm sure that's enough */
}
preview →

10 margin-top: 50% wrong

Percentages on margin are computed against the width. Resize the window. Watch the world drift.

#centerish { margin-top: 50%; }
preview →

11 padding-top: 384px wrong

Works on the developer's laptop. Their screen is 768px tall and 384px is exactly half. Your bug report was closed as "cannot reproduce."

#centerish { padding-top: 384px; }
preview →

12 calc(50vh - 8px) wrong

Assumes a 16px font and one specific text height. Zoom in. The illusion shatters.

#centerish { position: absolute; top: calc(50vh - 8px); }
preview →

13 vertical-align: middle on a div wrong

Written with conviction. Does absolutely nothing. The dev moves on.

#centerish { vertical-align: middle; }
preview →

14 the <center> tag wrong

It's literally called center. Centers horizontally only. The H is silent.

<center>am I centered?</center>
preview →

15 text-align: center, vertically wrong

Bold claim. Same energy as #14, with a CSS property attached.

body { text-align: center; }
preview →

16 align-content: center on a single-line flex wrong

Silently no-op. Documented. Still surprising. Still wrong.

.row {
  display: flex;
  align-content: center; /* needs flex-wrap to do anything */
}
preview →

Cursed (but it works)

17 the ghost element cursed

An invisible inline-block sibling, 100% tall, baseline-aligned. Nobody knows why it works. It does.

.box::before {
  content: "";
  display: inline-block;
  height: 100%;
  vertical-align: middle;
}
#centerish { display: inline-block; vertical-align: middle; }
preview →

18 writing-mode: vertical-rl + text-align: center cursed

Rotate the text-flow 90°, then center horizontally. Which is now vertically.

.box {
  writing-mode: vertical-rl;
  text-align: center; /* now this means vertically */
}
preview →

19 SVG <text y="50%" dominant-baseline="middle"> cursed

Skip CSS entirely. Render text in SVG. SVG centers, because SVG can do whatever it wants.

<svg width="100%" height="100vh">
  <text x="50%" y="50%" text-anchor="middle"
        dominant-baseline="middle">hi</text>
</svg>
preview →

20 <dialog>.showModal() cursed

The browser centers it for free. You didn't even ask. There's an ::backdrop now too. You're welcome?

<dialog open>am I centered?</dialog>
<script>document.querySelector('dialog').showModal()</script>
preview →

21 two divs in synergy works

Two divs working together in cross-functional alignment. The parent owns position. The child owns offset. Together they deliver verticality at scale. Q4 OKR achieved.

.parent {
  position: absolute;
  top: 50vh;
  left: 0; right: 0;
}
#centerish {
  margin: 0 auto;
  transform: translateY(-50%);
}
preview →

22 :target with scroll-padding cursed

Visit the page with #here in the URL. The browser scrolls. The padding does the rest. Works once per pageload.

html { scroll-padding-block: 50vh; }
:target { /* the centerish element, when fragmented */ }
preview →

23 position: sticky; top: 50vh cursed

Centered once scrolled.

#centerish { position: sticky; top: 50vh; }
preview →

24 <details> with the content in <summary> cursed

Disclosure widget as a centering primitive. Don't open it.

<details style="display:grid;place-items:center;height:100vh">
  <summary>am I centered?</summary>
</details>
preview →

25 canvas with textBaseline = "middle" cursed

Bitmap your text. Place it at (w/2, h/2). Achieve copy-paste protection as bonus.

const c = canvas.getContext('2d');
c.textBaseline = 'middle';
c.textAlign = 'center';
c.fillText('am I centered?', canvas.width/2, canvas.height/2);
preview →

HTML crimes

26 <br> spam crime

Eyeball it until it looks centered. Resize the window. Add more <br>. This is fine.

<br><br><br><br><br><br><br><br><br><br>
<br><br><br><br><br><br><br><br><br>
am I centered?
preview →

27 non-breaking space columns crime

Like <br> spam, but with whitespace pretending to be content.

&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
am I centered?
preview →

28 floats (CSS war crimes) crime

Floats were designed to wrap text around images. We're using one as a 50vh-tall spacer with a negative margin equal to half the content height. Together the math cancels and the content lands centered. The content height is hardcoded.

.spacer {
  float: left;
  height: 50vh;
  margin-bottom: -90px;        /* = -(content / 2) */
}
#centerish {
  clear: left;
  height: 180px;
  margin: 0 auto;
}
preview →

29 <marquee direction="up" behavior="alternate"> crime

Bounces past center forever. Catch the screenshot at exactly the right frame.

<marquee direction="up" behavior="alternate"
         height="100vh" scrollamount="3">
  am I centered?
</marquee>
preview →

30 <noscript> fallback crime

Centers correctly only when JavaScript is disabled.

<noscript>
  <style>#centerish{display:grid;place-items:center;height:100vh}</style>
</noscript>
<script>/* exists */</script>
preview →

31 50 nested <div>s, padding-top: 1% crime

Each div pushes a little. Together they push half.

<div><div><div><div><div>... (47 more) ...
  am I centered?
</div></div></div></div></div>
/* every div: padding-top: 1% */
preview →

32 <hr> on top, <hr> on bottom, flex: 1 each crime

Two horizontal rules act as struts. The rules push the content. Semantic content is now horizontal rules.

<div style="display:flex;flex-direction:column;height:100vh">
  <hr style="flex:1">
  am I centered?
  <hr style="flex:1">
</div>
preview →

33 <select size="21"> with the line at index 10 crime

Form widget as a layout primitive. Keyboard scrolls it. The OS picks the font for you.

<select size="21" style="height:100vh; width:100%">
  <option></option> <!-- x10 -->
  <option>am I centered?</option>
  <option></option> <!-- x10 -->
</select>
preview →

JS atrocities

34 requestAnimationFrame, every frame, forever js crime

Reads offsetHeight. Writes top. Then again. Then again. Then again.

function tick() {
  el.style.top = (innerHeight - el.offsetHeight) / 2 + 'px';
  requestAnimationFrame(tick);
}
tick();
preview →

35 setInterval(center, 16) js crime

Same as #34, less elegant, more drift. Sometimes 60fps. Sometimes 7.

setInterval(() => {
  el.style.top = (innerHeight - el.offsetHeight) / 2 + 'px';
}, 16);
preview →

36 MutationObserver re-centers on any DOM change js crime

Including its own writes. The observer triggers itself. Forever. Stop it before it learns.

const o = new MutationObserver(() => {
  el.style.top = (innerHeight - el.offsetHeight) / 2 + 'px';
});
o.observe(document.body, { attributes: true, subtree: true });
preview →

37 resize listener, no debounce, animated jitter js crime

Drag the corner of the window. Watch the content shimmy.

addEventListener('resize', () => {
  el.style.transition = 'top 200ms';
  el.style.top = (innerHeight - el.offsetHeight) / 2 + 'px';
});
preview →

38 WebGL: a single quad at clip-space (0,0) js crime

Compile a shader. Set up buffers. Render text as a texture. Three hundred lines. Centered.

// vertex shader: gl_Position = vec4(pos, 0, 1);
// quad spans (-0.4, -0.1) to (0.4, 0.1)
// text drawn to canvas, used as texture
// 50 LOC of WebGL boilerplate omitted
preview →

39 Web Worker posts the y-coordinate every 100ms js crime

Threading. For centering. Why? Because we can. Sleep peacefully.

// worker.js
setInterval(() => postMessage(window.innerHeight / 2), 100);
// main
worker.onmessage = e => el.style.top = e.data + 'px';
preview →

40 document.write a <style> mid-render js crime

Cursed even when the lighthouse score doesn't tank. Lighthouse score tanks.

<script>
  document.write('<style>#centerish{display:grid;'
    + 'place-items:center;height:100vh}</style>');
</script>
preview →

Galaxy-brain

41 @container query at exactly the demo's height galaxy

Add 1px to the container. The center evaporates.

.outer { container-type: size; height: 100vh; }
@container (height: 800px) {
  #centerish { /* the centering rules */ }
}
preview →

42 @media print galaxy

Centers correctly only when printed.

@media print {
  #centerish { display: grid; place-items: center; height: 100vh; }
}
preview →

43 CSS anchor positioning (anchor placed by hand) galaxy

Place a 0×0 invisible anchor at the center using top: 50vh. Then position the content relative to the anchor with anchor(center). Two new CSS specs to do what one already did.

.anchor {
  position: absolute;
  top: 50vh; left: 50vw;       /* anchor is centered the OLD way */
  width: 0; height: 0;
  anchor-name: --middle;
}
#centerish {
  position: absolute;
  position-anchor: --middle;
  top: anchor(center);          /* defer to the anchor we just placed */
  left: anchor(center);
  translate: -50% -50%;
}
preview →

44 mask-image gradient (only looks centered) galaxy

Content is at the top. A mask hides everything except a band across the middle. Visual deceit.

.page {
  mask-image: linear-gradient(transparent 40%, #000 40% 60%, transparent 60%);
}
preview →

45 text at 100vh, centered by being everywhere galaxy

Make the element AND its text 100vh tall. Pick a font-size that fills the viewport top to bottom. The text IS the page. Centered, by virtue of being everywhere.

#centerish {
  height: 100vh;
  width: 100vw;
  line-height: 1.3;  
  font-size: 7vh; /* TODO: explain calculation */
}
preview →

46 <input type="image"> as layout galaxy

It's a form control. It's an image. It's whatever you need it to be at 3am.

<input type="image" alt="am I centered?"
       style="position:absolute; top:50%; left:50%;
              transform:translate(-50%,-50%)">
preview →

47 font-size: 50vh; line-height: 0 galaxy

Type metrics to the rescue. Don't think about it too hard.

.box { font-size: 50vh; line-height: 0; }
/* content: a single · */
preview →

48 :has(:focus) toggle galaxy

Centers only when the user clicks the target. Otherwise the page just hangs out.

body:has(#centerish:focus) { display: grid; place-items: center; height: 100vh; }
preview →

49 <iframe srcdoc> with flexbox inside galaxy

Outsource centering to a child document. The iframe centers. The page contains an iframe.

<iframe srcdoc="<style>body{display:grid;place-items:center;
  height:100vh;margin:0}</style>am I centered?"></iframe>
preview →

50 Recursive iframe of itself galaxy

Each level shrinks 5%. The text is somewhere in there. Mathematically, in the limit, centered.

<!-- 50.html embeds 50.html?d=1 embeds 50.html?d=2 ... -->
<iframe src="50.html?d=N+1" style="width:95%;height:95%"></iframe>
preview →