CSS positioning steps outside of normal flow in different ways depending on the value used. This demo shows all five positioning models with labeled boxes so you can see exactly how each one behaves — and where it anchors.
Sticky note: The sticky row in this demo works because the scroll container has a fixed height and internal overflow scroll — the conditions sticky requires. Scroll inside that box to see it pin.
Fixed note: The fixed chip pins to the lower-right corner of the preview iframe (the iframe is the viewport in this context), which accurately demonstrates the concept.
What this covers
position: static — default; document flow places it
position: relative — keeps its flow slot, shifts with top/left
position: absolute — leaves flow; positions from nearest positioned ancestor
position: sticky — scrolls normally, then pins at a threshold inside its container
position: fixed — ignores scroll and pins to the viewport
<main class="stack">
<h1>Positioning Models</h1>
<p class="hint">This file compares static, relative, absolute, fixed, and sticky.</p>
<section class="card stack">
<h2>Relative + Absolute</h2>
<p class="hint">Each label below maps directly to one visible box and its CSS rule.</p>
<ul class="explain-list">
<li><strong>Static:</strong> default flow positioning. It stays where normal document flow puts it.</li>
<li><strong>Relative:</strong> keeps its flow space, then shifts by <code>top</code>/<code>left</code>.</li>
<li><strong>Absolute:</strong> leaves flow and positions itself from the nearest positioned ancestor.</li>
</ul>
<!-- Absolute box positions itself against the nearest positioned ancestor -->
<div class="position-stage position-demo">
<div class="box static-box">
Static (default)
<span class="meta">position: static;</span>
</div>
<div class="box relative-box">
Relative offset
<span class="meta">position: relative; top: 12px; left: 16px;</span>
</div>
<div class="box absolute-box">
Absolute corner
<span class="meta">position: absolute; top: 16px; right: 16px;</span>
</div>
</div>
</section>
<section class="card stack">
<h2>Sticky</h2>
<p class="callout">Your goal: compare container-based sticking (<code>sticky</code>) with viewport-based pinning (<code>fixed</code>).</p>
<ul class="explain-list">
<li><strong>Sticky row:</strong> scrolls normally, then pins at <code>top: 0</code> inside this box.</li>
<li><strong>Sticky stage:</strong> has fixed height + internal scroll so sticky behavior is observable.</li>
<li><strong>Fixed chip:</strong> ignores container scroll and stays attached to viewport corner.</li>
</ul>
<!-- Scroll this area: sticky item pins to the top of its scrolling container -->
<div class="position-stage sticky-stage">
<div class="sticky-row">
I stay at the top while this box scrolls.
<span class="meta">position: sticky; top: 0;</span>
</div>
<div class="scroll-column">
<div class="box">Row 1</div>
<div class="box">Row 2</div>
<div class="box">Row 3</div>
<div class="box">Row 4</div>
<div class="box">Row 5</div>
<div class="box">Row 6</div>
<div class="box">Row 7</div>
<div class="box">Row 8</div>
<div class="box">Row 9</div>
<div class="box">Row 10</div>
<div class="box">Row 11</div>
<div class="box">Row 12</div>
</div>
</div>
</section>
</main>
<!-- Fixed element is attached to viewport, not a section/container -->
<div class="fixed-chip">Fixed to viewport</div> /* lesson.css base */
/* Shared design tokens used by all CSS demos.
Project setup pattern: define tokens once in :root, then reuse with var(--token). */
:root {
/* Surface/background colors */
--bg: #f8fafc;
--card: #ffffff;
/* Text colors */
--ink: #111827;
--subtle: #4b5563;
/* Border and accent colors */
--line: #d1d5db;
--accent: #0f766e;
}
/* Make element sizing predictable: width includes border/padding */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Base page styles reused by each lesson page */
body {
margin: 0;
font-family: "Inter", "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
color: var(--ink);
background: var(--bg);
line-height: 1.5;
}
a:focus-visible,
button:focus-visible,
input:focus-visible,
textarea:focus-visible,
select:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 2px;
}
/* Content container */
main {
max-width: 960px;
margin: 0 auto;
padding: 2rem 1rem 3rem;
}
/* Generic content card */
.card {
background: var(--card);
border: 1px solid var(--line);
border-radius: 10px;
padding: 1rem;
}
/* Vertical rhythm helper: adds top margin to siblings only */
.stack > * + * {
margin-top: 0.75rem;
}
/* Secondary explanatory text */
.hint {
color: var(--subtle);
font-size: 0.95rem;
}
/* Short explanatory callout block for each lesson section */
.callout {
border-left: 3px solid var(--accent);
background: #f0fdfa;
padding: 0.6rem 0.75rem;
border-radius: 6px;
color: #134e4a;
font-size: 0.92rem;
}
/* Bullet list used to explain what each demo element is showing */
.explain-list {
margin: 0;
padding-left: 1.2rem;
}
.explain-list li + li {
margin-top: 0.35rem;
}
/* Inline CSS rule label shown inside demo elements */
.meta {
display: block;
margin-top: 0.3rem;
font-size: 0.72rem;
color: #475569;
}
/* Optional button helper class (not required in every demo) */
.button {
display: inline-block;
border: 0;
border-radius: 8px;
background: var(--accent);
color: #fff;
padding: 0.5rem 0.75rem;
text-decoration: none;
}
/* lesson-specific */
.position-stage {
position: relative;
min-height: 220px;
border: 1px solid var(--line);
border-radius: 8px;
padding: 1rem;
background: #fff;
overflow: auto;
}
.position-stage.position-demo {
padding-top: 2rem;
}
/*
Extra spacing so static and relative examples are visually distinct.
*/
.static-box {
margin-bottom: 2.75rem;
}
/*
Sticky works when there is a real scrolling context.
This fixed height + overflow creates that context.
*/
.sticky-stage {
height: 240px;
overflow-y: auto;
}
.box {
width: 130px;
padding: 0.5rem;
border-radius: 8px;
border: 1px solid #9ca3af;
background: #f3f4f6;
}
.relative-box {
position: relative;
top: 12px;
left: 16px;
background: #dbeafe;
border-color: #93c5fd;
z-index: 1;
}
/*
This overlay marks where the relative box would sit in normal flow
before top/left offsets are applied.
*/
.relative-box::before {
content: "";
position: absolute;
top: -12px;
left: -16px;
width: 100%;
height: 100%;
border: 1px dashed #93c5fd;
border-radius: 8px;
pointer-events: none;
}
.relative-box::after {
content: "Original flow slot";
position: absolute;
bottom: -1.35rem;
left: -16px;
font-size: 0.72rem;
color: #1d4ed8;
background: #fff;
padding: 0 0.2rem;
white-space: nowrap;
}
.absolute-box {
position: absolute;
top: 16px;
right: 16px;
background: #fee2e2;
border-color: #fca5a5;
}
.fixed-chip {
position: fixed;
right: 16px;
bottom: 16px;
background: #ccfbf1;
border: 1px solid #5eead4;
border-radius: 999px;
padding: 0.5rem 0.75rem;
font-size: 0.875rem;
}
.sticky-row {
position: sticky;
top: 0;
z-index: 1;
background: #fef9c3;
border: 1px solid #fde047;
border-radius: 8px;
padding: 0.5rem;
}
.scroll-column {
display: grid;
gap: 0.5rem;
margin-top: 0.75rem;
}