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.
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 →