Module: CSS
CSS·026·9 MIN READ

026: Positioning

TOPICS COVERED: Positioning

Learning outcomes

By the end, you can distinguish static, relative, absolute, fixed, and sticky positioning; identify containing blocks; use inset and z-index; preserve logical source order; and choose positioning only for genuine overlays or anchored details.

Prerequisites and retrieval

Retrieve normal flow, block/inline behavior, and the four box layers. Before moving anything, confirm the page works in source order. Positioning changes how boxes participate in flow; it is not the primary tool for page columns.

Terminology

Mental model: preserve a seat or float above the document

static is normal positioning; inset properties have no positioning effect. relative keeps the element's original seat in flow but can offset its painted box and establishes a containing block for absolutely positioned descendants. absolute leaves normal flow and positions against its containing block. fixed usually positions against the viewport. sticky behaves in flow until scrolling reaches a specified inset, then is constrained by its scrolling ancestor.

Positioning does not automatically mean “in front.” Painting and stacking contexts decide overlap. Huge z-index values do not escape an ancestor stacking context. Keep stacking simple and local.

Beginner example: card badge

html
<article class="project-card">
  <p class="project-card__badge">Featured</p>
  <h2>Library finder</h2>
  <p>Search opening hours and accessible routes.</p>
  <a href="#">Read case study</a>
</article>
css
.project-card {
  position: relative;
  max-inline-size: 36rem;
  padding: 2.75rem 1rem 1rem;
  border: 1px solid rgb(203 213 225);
  border-radius: 0.75rem;
  background: white;
}

.project-card__badge {
  position: absolute;
  inset-block-start: 0.75rem;
  inset-inline-end: 0.75rem;
  margin: 0;
  padding: 0.2em 0.6em;
  border-radius: 999px;
  color: rgb(30 58 138);
  background: rgb(219 234 254);
  font-weight: 700;
}

The positioned card establishes the badge's containing block. The badge is out of flow, so the card reserves extra top padding to prevent collision. Remove position: relative and observe the badge seek another containing block, often appearing far away. Logical insets support writing direction.

The badge text remains in sensible HTML order. If CSS fails, “Featured” still precedes the project heading.

Intermediate example: sticky section navigation

html
<nav class="section-nav" aria-label="Portfolio sections">
  <a href="#work">Work</a>
  <a href="#process">Process</a>
  <a href="#contact">Contact</a>
</nav>
css
.section-nav {
  position: sticky;
  inset-block-start: 0;
  z-index: 10;
  padding: 0.75rem 1rem;
  background: rgb(255 255 255 / 96%);
  border-block-end: 1px solid rgb(203 213 225);
}

.section-nav a {
  display: inline-block;
  padding: 0.5rem;
}

Sticky requires an inset such as inset-block-start: 0. It sticks relative to its nearest ancestor with a scrolling mechanism, created by values such as overflow: auto, scroll, or hidden, and remains within its containing block. overflow: clip clips paint but does not create a scrolling mechanism or scroll container; use it only when clipping is independently intended, not as an explanation for which ancestor a sticky element follows. Ensure sticky content does not cover focused content or fragment targets; test zoom, narrow widths, and long labels.

A skip link is a justified off-screen position pattern:

css
.skip-link {
  position: absolute;
  inset-inline-start: 1rem;
  inset-block-start: 0;
  transform: translateY(-150%);
  padding: 0.75rem 1rem;
  background: white;
  color: rgb(30 64 175);
}
.skip-link:focus { transform: translateY(0); z-index: 100; }

It remains keyboard reachable and becomes visible on focus. Do not use display: none for a skip link.

Optional advanced example: fixed utility control

css
.back-to-top {
  position: fixed;
  inset-inline-end: 1rem;
  inset-block-end: 1rem;
  z-index: 20;
  padding: 0.75rem;
  border: 2px solid currentColor;
  background: white;
}

Fixed controls can cover content, software keyboards, or browser UI. Add one only if it provides real value, verify its target and label, and ensure page functionality does not depend on it. A transformed ancestor can change fixed positioning behavior in some cases.

Stacking and overlap

Two positioned elements can overlap:

css
.project-card { position: relative; z-index: 0; }
.project-card__badge { position: absolute; z-index: 1; }

This creates local, understandable levels. Properties including positioned z-index, opacity below 1, and transform can create stacking contexts. If z-index: 999999 does nothing, inspect ancestors rather than increasing it again.

Mistakes, debugging, and DevTools

  • Absolutely positioning every section: content no longer reserves space and overlaps at different sizes.
  • Forgetting a positioned ancestor: absolute offsets reference an unexpected box.
  • Sticky with no inset: there is no threshold.
  • Sticky inside an unintended overflow ancestor: it sticks within that ancestor.
  • Using fixed headers without compensating for covered anchors and focus.
  • Using top/left for visual layout that Flexbox or Grid should handle.
  • Solving stacking with enormous arbitrary numbers: inspect stacking contexts.
  • Moving focusable items visually away from DOM order: keyboard focus appears to jump.

DevTools badges and Layout panels identify sticky and scroll containers in modern browsers. Inspect position, inset values, containing dimensions, and ancestor overflow. Use the Elements tree to find stacking-context triggers. Scroll, zoom, and use Tab while watching overlays; a screenshot at one width is insufficient.

Accessibility and performance

Visual order should follow DOM order. Overlays must not obscure focused controls or text at 200% and 400% zoom. Sticky headers should remain compact when text wraps. Skip links need strong focus contrast. Fixed controls require accessible names and adequate targets.

Animating top and left repeatedly can trigger layout; transforms are often smoother, but motion still needs restraint. For nonessential animation, honor prefers-reduced-motion. Static positioning itself is not a performance issue; unnecessary overlapping layers and effects can be.

Deep dive: containing blocks decide what offsets mean

Absolute positioning is not “relative to the nearest parent” in general. It is positioned against its containing block, which is established by specific ancestor/layout conditions.

Common pattern:

html
<article class="card">
  <span class="card__badge">New</span>
  ...
</article>
css
.card {
  position: relative;
}

.card__badge {
  position: absolute;
  inset-block-start: 0.75rem;
  inset-inline-end: 0.75rem;
}

position: relative leaves the card in normal flow but establishes a containing block for the absolutely positioned badge.

Use logical offsets (inset-inline-end) when the design should adapt to writing direction instead of assuming “right”.

Deep dive: each positioning mode

Static

css
.element {
  position: static;
}

Default. Insets and z-index generally do not position it like a positioned box.

Relative

css
.element {
  position: relative;
  inset-block-start: 0.25rem;
}

The element keeps its original space in flow; visual offset does not cause surrounding content to reflow into the old position.

Absolute

css
.element {
  position: absolute;
  inset: 0;
}

The element is removed from normal flow and positioned against its containing block.

Fixed

css
.utility {
  position: fixed;
  inset-inline-end: 1rem;
  inset-block-end: 1rem;
}

Typically fixed to the viewport, though transforms and related properties on ancestors can change containing-block behavior.

Sticky

css
.toc {
  position: sticky;
  inset-block-start: 1rem;
}

Sticky behaves like normal flow until a scroll threshold is reached, within the constraints of its scroll container and containing block.

Worked example: why sticky “does not work”

css
.layout {
  overflow: hidden;
}

.sidebar {
  position: sticky;
  top: 1rem;
}

A nearby ancestor with overflow behavior can establish the scroll container or clipping context that sticky uses, making the result different from “stick to the viewport.”

Debug:

  1. Inspect ancestor overflow values.
  2. Check whether the sticky box is taller than available scroll space.
  3. Check whether its parent ends before it has room to remain stuck.
  4. Confirm a non-auto inset such as top/inset-block-start is present.

Do not respond by changing sticky to fixed; fixed positioning has different semantics and can obscure content.

Deep dive: stacking contexts

z-index does not create one global number line. Elements are painted inside stacking contexts.

Common stacking-context triggers include certain combinations of:

  • positioned elements with non-auto z-index;
  • opacity below 1;
  • transforms;
  • filters;
  • isolation;
  • some containment properties.

Example:

css
.card {
  position: relative;
  z-index: 10;
}

.modal {
  position: fixed;
  z-index: 9999;
}

A huge modal z-index can still lose if it is trapped inside an ancestor stacking context that itself sits below another context.

Worked example: stable layering scale

css
:root {
  --z-base: 0;
  --z-dropdown: 20;
  --z-sticky: 30;
  --z-overlay: 40;
  --z-modal: 50;
  --z-toast: 60;
}
css
.site-header {
  position: sticky;
  top: 0;
  z-index: var(--z-sticky);
}

.modal-backdrop {
  position: fixed;
  inset: 0;
  z-index: var(--z-overlay);
}

.modal {
  position: fixed;
  z-index: var(--z-modal);
}

Tokens do not solve stacking-context bugs by themselves, but they document intended layers and discourage random values like 99999999.

Positioning anti-pattern: using absolute positioning for page layout

Fragile:

css
.sidebar {
  position: absolute;
  left: 70%;
  top: 10rem;
  width: 25%;
}

Content height, zoom, localization, and responsive changes can cause overlap.

Prefer:

css
.layout {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(16rem, 1fr);
  gap: 2rem;
}

Use positioning for overlays, anchored badges, sticky controls, and deliberate layers—not as a substitute for normal layout relationships.

Stacking-debug sequence

  1. Identify the two elements that overlap incorrectly.
  2. Find each element's nearest stacking-context ancestor.
  3. Compare those ancestor contexts, not just child z-index values.
  4. Look for unexpected transforms, opacity, filters, or isolation.
  5. Remove unnecessary stacking-context triggers before raising numbers.

Tiered exercises

Checkpoint: containing-block investigation

Build three nested boxes named outer, card, and badge. Give only outer position: relative, then absolutely position badge with inset: 0. Observe that it uses outer even though card is its immediate parent. Move position: relative to card and observe the reference change. This proves that “absolute means relative to the parent” is incomplete; it uses the containing block established by the nearest qualifying ancestor.

Next, scroll a sticky element through a short section. It cannot stick beyond the boundary of its containing block. Add overflow: auto to an ancestor with a constrained height and watch the sticky element respond to that scrolling box. Remove the test declarations after you can explain the behavior.

For stacking, create two overlapping positioned cards. Give one child z-index: 100 inside an ancestor stacking context at level 1, and place the other ancestor at level 2. The child cannot leap above the level-2 sibling context. Inspect ancestors for transform, opacity, and positioned z-index; solving the hierarchy is better than inventing larger numbers.

Finally, perform an overlap audit at every viewport width: Tab through controls, follow in-page anchors, enlarge text, and scroll to the bottom. Fixed/sticky content must not conceal the focused item, validation message, footer, or destination heading. If preserving visibility requires complicated offsets, simplify or remove the overlay.

Use relative offsets carefully. A relatively positioned element keeps its original flow space, so moving it visually leaves a gap and can paint over neighbors. This is useful for a tiny intentional nudge or for creating an absolute containing block, but not for rearranging sections. Transforms similarly change painting without making normal flow reserve the transformed destination.

Positioned inset values can use logical properties. inset-inline-end maps to the appropriate physical side for writing direction, whereas right always means the physical right. For reusable international components, logical insets usually express the intended relationship more accurately. Test right-to-left direction if the project may be localized.

A positioned element with z-index: auto participates in painting differently from one establishing an explicit local level. Introduce z-index only where overlap exists, and document a small scale such as base, sticky header, modal, and skip link. A modal also requires JavaScript focus management, keyboard dismissal, labeling, and background interaction control; CSS stacking alone does not create an accessible dialog.

Foundation: Add an absolute “Featured” badge anchored to a relative project card. Reserve enough card space for it.

Core: Create a sticky section navigation with a background and local stack level. Test scrolling, narrow width, and keyboard focus.

Stretch: Add a skip link hidden by transform until focus. Explain why display: none would break its purpose.

html
<a class="skip-link" href="#main">Skip to main content</a>
<nav class="section-nav" aria-label="Portfolio sections">...</nav>
<main id="main">
  <article class="project-card"><p class="project-card__badge">Featured</p><h2>Library finder</h2></article>
</main>
css
.skip-link { position: absolute; inset: 0 auto auto 1rem; transform: translateY(-150%); padding: .75rem 1rem; background: white; }
.skip-link:focus { transform: translateY(0); z-index: 100; }
.section-nav { position: sticky; inset-block-start: 0; z-index: 10; padding: .75rem 1rem; background: rgb(255 255 255 / 96%); }
.project-card { position: relative; padding: 2.75rem 1rem 1rem; }
.project-card__badge { position: absolute; inset-block-start: .75rem; inset-inline-end: .75rem; margin: 0; }

Recap and exit questions

Positioning is for deliberate offsets, anchors, and overlays, not primary page layout. Relative positioning preserves flow space; absolute and fixed usually do not; sticky transitions while remaining constrained.

  1. Which position values remove a box from normal flow?
  2. Why does an absolute badge need a positioned ancestor?
  3. What two conditions commonly make sticky fail?
  4. Why can a large z-index still lose?
  5. What accessibility checks apply to fixed and sticky content?

Official references