Positioning

CSS · Lesson 21 of 55 7 min read Updated

Most of the time, elements should stay in the normal flow of the page, with Flexbox and Grid arranging them. But some things need to break out: a "New" badge on the corner of a card, a header that stays visible while you scroll, a chat button fixed to the bottom of the screen, a full-screen overlay. That is the job of the position property.

In this lesson you will learn the five position values, the inset shorthand, what a containing block is, and how z-index decides which element appears on top.

The five position values

ValueStays in the flow?Positioned relative to
staticYes (default)Not positioned; offsets are ignored
relativeYes, its space is keptIts own normal position
absoluteNo, other elements ignore itThe nearest positioned ancestor
fixedNoThe viewport (browser window)
stickyYesNormal, until it reaches a set edge while scrolling, then it sticks

The offset properties top, right, bottom and left move a positioned element. The shorthand inset sets all four at once, in the same clockwise order as margins: inset: 0 means all four are 0. Logical versions such as inset-inline-start also exist; see Logical Properties.

relative and absolute

position: relative on its own barely changes anything: offsets nudge the element from where it would normally be, and its original space stays reserved. Its real use is as an anchor for absolutely positioned children.

position: absolute takes an element out of the normal flow. Other elements behave as if it is not there. Its offsets are measured from its containing block: the nearest ancestor whose position is anything other than static. If there is none, it uses the whole page.

That gives the most common positioning pattern: relative on the parent, absolute on the child.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>relative and absolute</title>
  <style>
    *, *::before, *::after { box-sizing: border-box; }
    body { font-family: system-ui, sans-serif; padding: 1.5rem; }
    .card {
      position: relative;          /* the anchor */
      max-width: 18rem;
      border: 1px solid #d0d7e2;
      border-radius: 12px;
      padding: 1rem;
    }
    .card img { width: 100%; height: auto; display: block; border-radius: 8px; }
    .badge {
      position: absolute;          /* taken out of the flow */
      top: -0.75rem;
      right: -0.75rem;
      background: oklch(58% 0.2 30);
      color: white;
      font-weight: bold;
      padding: 0.3rem 0.7rem;
      border-radius: 999px;
    }
  </style>
</head>
<body>
  <div class="card">
    <span class="badge">New</span>
    <img src="https://course.ethicscomputer.in/assets/samples/landscape-400.svg" alt="Green hills and a river near Munnar" width="400" height="250">
    <h3>Munnar weekend trip</h3>
    <p>2 nights, 3 days, from ₹6,499 per person.</p>
  </div>
</body>
</html>

The badge sits on the top-right corner of the card, slightly outside it, because its offsets are measured from the card. Remove position: relative from .card and run again: the badge jumps to the corner of the whole page, because no ancestor is positioned any more.

Full-size overlays

An absolutely positioned element with inset: 0 stretches to cover its containing block exactly. That is the simplest way to put a dark layer or a caption over an image:

CSS
.photo {
  position: relative;
}
.photo .overlay {
  position: absolute;
  inset: 0;                         /* top, right, bottom and left all 0 */
  background: rgb(0 0 0 / 45%);
  display: grid;
  place-content: center;
  color: white;
}

fixed and sticky

position: fixed attaches an element to the viewport. It stays in the same place on screen while the page scrolls, which suits a floating "back to top" or help button. Because a fixed element is out of the flow, it can cover content; leave space for it.

position: sticky is a hybrid. The element scrolls normally until it reaches the offset you set, such as top: 0, and then sticks there while its parent is still on screen. When the parent scrolls away, the sticky element goes with it. Sticky headers, sticky table headings and sticky sidebars all use this.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>sticky and fixed</title>
  <style>
    body { font-family: system-ui, sans-serif; margin: 0; }
    header {
      position: sticky;
      top: 0;
      background: #1d3557;
      color: white;
      padding: 0.75rem 1rem;
    }
    main { padding: 1rem; }
    section { min-height: 60vh; border-bottom: 1px solid #dde3ea; }
    h2 { position: sticky; top: 3rem; background: #f1f4f8; padding: 0.4rem; }
    .help {
      position: fixed;
      right: 1rem;
      bottom: 1rem;
      background: oklch(55% 0.15 150);
      color: white;
      border: none;
      border-radius: 999px;
      padding: 0.7rem 1.2rem;
    }
  </style>
</head>
<body>
  <header>Lucknow City Guide</header>
  <main>
    <section><h2>Places to see</h2><p>Bara Imambara, Rumi Darwaza and the Residency.</p></section>
    <section><h2>Food to try</h2><p>Galouti kebabs, kulfi and basket chaat.</p></section>
    <section><h2>Getting around</h2><p>The metro, autos and cycle rickshaws.</p></section>
  </main>
  <button class="help">Need help?</button>
</body>
</html>

Scroll inside the result. The header sticks to the top. Each section heading sticks just below it, then gets pushed away when its section ends, because a sticky element never leaves its parent. The help button stays fixed in the corner the whole time.

If position: sticky seems to do nothing, check three things: it needs an offset such as top: 0; its parent must be taller than the element; and no ancestor between it and the scrolling area should have overflow: hidden (see Sizing, Aspect Ratio & Overflow).

z-index and stacking

When positioned elements overlap, z-index decides which is on top: a higher number is closer to the viewer. It works on positioned elements (anything except static) and on flex and grid items.

There is a catch that confuses everyone at least once: the stacking context. Some properties turn an element into a closed group for stacking. Its children can only be ordered inside that group; a child with z-index: 9999 can never rise above an element outside the group that sits higher. These create a stacking context:

  • a positioned element with a z-index other than auto;
  • opacity less than 1;
  • transform, filter and some other visual effects;
  • isolation: isolate, whose only job is to create one.
HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Stacking contexts</title>
  <style>
    body { font-family: system-ui, sans-serif; padding: 1rem; }
    .box { position: relative; padding: 1rem; width: 14rem; color: white; }
    .header { z-index: 2; background: #1d3557; }
    .content { z-index: 1; background: #457b9d; margin-top: -0.5rem; }
    .menu { position: absolute; top: -2.5rem; left: 8rem; z-index: 9999; background: #e63946; padding: 0.5rem; }
  </style>
</head>
<body>
  <div class="box header">Header (z-index 2)</div>
  <div class="box content">
    Content (z-index 1)
    <div class="menu">Menu (z-index 9999)</div>
  </div>
</body>
</html>

The red menu has z-index: 9999, yet the header covers it. The menu lives inside .content, which formed a stacking context at level 1, and the whole group sits below the header at level 2. To fix it, raise .content itself, or move the menu out of it. Keep z-index values small and planned (1, 2, 10), not an arms race of ever larger numbers.

Common mistakes

  • Absolute child, unpositioned parent. The child positions itself against the page. Add position: relative to the parent.
  • Using absolute positioning for layout. Absolute elements ignore their neighbours and break when content changes. Use Flexbox or Grid for layout.
  • Sticky without an offset. position: sticky needs top, bottom or another inset to know where to stick.
  • Giant z-index values. If 9999 does not work, the problem is a stacking context, not the number.
  • Fixed elements covering content. Add padding to the page so a fixed bar does not hide the last lines.

Key points

  • static is the default; offsets only work on positioned elements.
  • relative keeps the element in the flow and makes it the containing block for absolute children.
  • absolute removes the element from the flow and positions it against the nearest positioned ancestor.
  • fixed sticks to the viewport; sticky sticks while scrolling but never leaves its parent.
  • inset: 0 sets all four offsets at once, which is ideal for overlays.
  • z-index only orders elements within the same stacking context.

Exercise

Create a product image for an online art print shop with a "20% off" label pinned to its bottom-left corner, using relative and absolute. Then add a sticky top bar with the shop name, and a fixed "Chat with us" button in the bottom-right corner. Add enough content to scroll so you can test both.

Show solution
HTML
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>Art print shop</title>
  <style>
    body { font-family: system-ui, sans-serif; margin: 0; }
    .bar { position: sticky; top: 0; background: #3d1f4f; color: white; padding: 0.75rem 1rem; }
    main { padding: 1rem; min-height: 150vh; }
    .product { position: relative; max-width: 16rem; }
    .product img { width: 100%; height: auto; display: block; }
    .label {
      position: absolute;
      left: 0.5rem;
      bottom: 0.5rem;
      background: #ffd166;
      padding: 0.2rem 0.6rem;
      font-weight: bold;
    }
    .chat {
      position: fixed;
      right: 1rem;
      bottom: 1rem;
      padding: 0.7rem 1.1rem;
      border: none;
      border-radius: 999px;
      background: #2a9d8f;
      color: white;
    }
  </style>
</head>
<body>
  <div class="bar">Isha Art Prints, Jaipur</div>
  <main>
    <div class="product">
      <img src="https://course.ethicscomputer.in/assets/samples/portrait.svg" alt="Art print of an old brick clock tower" width="400" height="600">
      <span class="label">20% off</span>
    </div>
    <p>Clock tower art print, A3 size, ₹1,299.</p>
  </main>
  <button class="chat">Chat with us</button>
</body>
</html>

The label is placed against .product because that is its nearest positioned ancestor, while the bar and button are placed against the scrolling page and the viewport.

Quick Quiz

1. An absolutely positioned element has no positioned ancestors. What is it positioned against?

2. What does inset: 0 mean?

3. A sticky heading stops sticking when its section scrolls away. Why?

4. A dropdown has z-index: 9999 but stays behind a header with z-index: 2. What is the likely cause?

Learn CSS with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call