Skip to content
Chapter 20Lesson 7

Position and inset utilities

How to use Tailwind's position and inset utilities to take elements like badges, sticky headers, and toasts out of normal flow and pin them where you want.

You’re building a SaaS dashboard and the design calls for a small “Saved” toast that appears in the bottom-right corner of the screen, floating above everything, no matter where the user has scrolled. It’s a reasonable ask, so you reach for what you know: flex, grid, a bit of margin. None of it works. margin: auto won’t push the toast to the bottom, because the parent isn’t tall enough to push against. Flex aligns things inside a container, but you want this toast to ignore its container entirely and stay fixed to the screen. The tools you have so far simply aren’t built for that.

The 2026 answer is three utilities: fixed bottom-4 right-4 z-50. That string pins a toast to the bottom-right of the viewport, and you’ll find it in every real SaaS app shipping today. Two things in it are worth a question. Why fixed and not absolute, and what is that z-50 doing? By the end of this lesson you’ll understand all five position modes, you’ll know exactly what each one anchors against, and you’ll build badges, sticky headers, toasts, and drawers without hitting the “why is it in the wrong corner” bug that can cost you an afternoon the first time you meet it.

There’s one connection to make before we start. Every element you’ve laid out so far, whether a flex item, a grid item, or a stacked block, has lived in normal flow . Flow is what makes a paragraph push the next paragraph down, and what makes a flex item leave room for its neighbors. position is how a small, specific set of elements opt out of flow, and that set stays small: most of your layout is still flex and grid. You reach for position only for the handful of elements that genuinely need to escape, like a badge on a corner, a header that sticks while you scroll, or a toast pinned to the screen.

There are five values for the position property, and the way to keep them straight is to ask a single question of each instead of memorizing five definitions: what is this element positioned against? Once you know what each one is measured from, the offsets (top, left, and the rest, which we’ll get to shortly) stop being mysterious.

Here are the five, framed that way:

  • static is the default. The element goes exactly where normal flow puts it, and offsets don’t apply at all. Every element you’ve styled so far has been static; you’ve just never had to say so.
  • relative stays in flow, so its original slot is preserved and its siblings don’t move, but offsets now shift it visually from that slot. Its main use in 2026 isn’t that nudge, though. It’s to become an anchor for absolute children inside it, which is the idea the rest of this lesson builds on.
  • absolute leaves flow entirely. It reserves no space, so its siblings collapse together as if it were deleted, and it’s positioned against the nearest positioned ancestor. This is the badge-and-overlay tool.
  • fixed also leaves flow, but it’s positioned against the viewport, the browser window itself, and it stays put while the page scrolls. This is the toast and floating-action-button tool.
  • sticky is the hybrid. It behaves like relative, in flow and holding its slot, until you scroll it to a defined offset, then it behaves like fixed, but only within its parent’s bounds. This is the sticky-header tool, and it’s the trickiest of the five, so it gets its own section later.

Tailwind gives you one utility per value, named identically to the CSS: static, relative, absolute, fixed, sticky. No surprises there.

The most useful split inside those five is whether the element is in flow or out of flow . static, relative, and sticky before it sticks all stay in flow: they hold their slot. absolute and fixed are out of flow: they reserve nothing, and the layout closes up around them. This distinction is worth getting solid first, because it explains why dropping an absolute element into a row doesn’t shove the row’s other items aside. There’s no slot left for it to shove against.

Sort each position mode by whether it reserves space in normal flow. Drag each item into the bucket it belongs to, then press Check.

Stays in flow Reserves a slot; siblings lay out around it
Leaves flow Reserves no space; siblings close up around it
static
relative
sticky
absolute
fixed

The one that trips people up is sticky. It lives in the stays column because it holds its normal slot right up until the scroll threshold, and only then does it behave like fixed. The split is otherwise clean: static and relative keep their slot, while absolute and fixed reserve nothing and let the layout close up around them.

Reading definitions only gets you so far with position, because the whole point is motion: the way a box’s relationship to its neighbors changes the instant you change its mode. So before we go deeper, it’s worth playing with that motion directly.

The playground below has one highlighted box sitting among three plain sibling boxes inside a bordered parent. The select changes the highlighted box’s position, and the two sliders set its top and left offsets. Switch between the modes and watch what happens to the siblings:

  • On static, the box sits in its normal slot and the offsets do nothing. Drag the sliders all you want and it won’t budge. That’s the default, and it’s why you never had to think about position until now.
  • Flip to relative and the box keeps its slot, so the siblings don’t move, but now the offsets shift it visually away from where it sits. Notice the gap it leaves behind: its original space is still reserved.
  • Flip to absolute and watch the siblings snap together to fill the hole. The box is out of flow now, so it reserves nothing, and the offsets now measure from the parent’s corner rather than its old slot. (Why the parent’s corner specifically? That’s the very next section.)

That contrast, where relative leaves a hole and absolute closes it, is the in-flow/out-of-flow split made visible. Slide the offsets around in each mode until the difference is obvious.

The accent box is the first of four in a flex row; the three grey siblings are plain and static. On static the offsets do nothing. On relative the box shifts but keeps its slot, leaving the gap behind. On absolute it leaves flow, the siblings close the gap, and the offsets now measure from the dashed parent's corner.

What an absolute element is positioned against

Section titled “What an absolute element is positioned against”

This one idea is what makes positioning predictable rather than fiddly. When you give an element position: absolute and then set top and left, the browser has to measure those offsets from something. That something is the containing block .

The rule for an absolute element is precise: its offsets are measured from the nearest ancestor that is itself positioned, meaning the nearest ancestor with position set to relative, absolute, fixed, or sticky. The browser walks up the DOM tree from the element, parent by parent, looking for the first one that’s positioned. That one becomes the containing block. There’s a catch worth knowing in advance. If the browser walks all the way up and finds no positioned ancestor, it falls back to the initial containing block , which is near enough the whole viewport.

That fallback is the source of the most common position bug in real code, and you will hit it. Picture building a product card with a small “New” badge in its top-right corner. You drop an absolute badge inside the card, set top-2 right-2, and the badge lands in the top-right corner of the entire page instead of the card. You stare at it, and nothing about your markup looks wrong.

The reason follows straight from the rule: nothing in the card is positioned. The badge’s absolute made the browser walk up looking for a positioned ancestor, it found none, so it anchored the badge to the viewport. The fix is a single class, relative on the card, which gives the badge a positioned ancestor to find before it climbs any higher.

src/components/PlanCard.tsx
<article className="rounded-xl border p-4">
<span className="absolute top-2 right-2 rounded-full bg-blue-600 px-2 text-xs text-white">
New
</span>
<h3>Starter plan</h3>
</article>

The badge finds no anchor. Nothing on the card is positioned, so the absolute span walks up the tree, finds nothing, and lands in the page’s top-right corner instead of the card’s.

This pairing is worth making automatic, because you’ll type it constantly: a relative parent plus an absolute child is the basic unit of corner-positioning. Whenever you want to pin something to a corner or fill a region of an element, the parent gets relative and the child gets absolute. The relative parent sets no offsets of its own; its entire job is to be the anchor. Experienced developers don’t pause on this. The moment they write absolute on a child, they’ve already added relative to the parent.

The diagram below makes the “walk up the tree” mechanic literal. On the left, an absolute child sits inside a plain card with nothing positioned around it; follow the arrow and it shoots straight past the card, past everything, up to the viewport edge. On the right, the same child sits inside a card marked relative, so the arrow stops at the card, because the card is the first positioned ancestor the browser finds.

viewport
child
card static
anchors to the viewport
viewport
card relative
child
anchors to the card
An absolute element climbs the tree until it finds a positioned ancestor. With nothing positioned (left) the climb runs all the way out to the viewport corner; a relative parent (right) stops the climb at the card.

There’s one exception to the “fixed anchors to the viewport” rule. It belongs to a later lesson, so we won’t unpack it here, but it’s worth naming once so you recognize it later. A fixed element normally ignores its ancestors and anchors to the viewport. The exception is when one of its ancestors has a transform, filter, or perspective set. That ancestor silently becomes the containing block instead, so your fixed modal ends up positioned relative to some transformed parent halfway down the page. The lesson on stacking context and z-index, later in this chapter, runs into the same problem from a different symptom. The fix in both cases is to portal the element out to <body> so it has no such ancestors, a technique you’ll meet in the chapter on portals and modals. For now, the one thing to carry forward is this: a transform on an ancestor can capture a fixed child.

Now that you know what an element is positioned against, you need the vocabulary for where on that containing block to place it. That’s the inset-* family, named for the CSS inset shorthand that groups the four offset properties (top, right, bottom, left). They all draw from the same --spacing scale you’ve used for padding and margin all chapter, so top-4 is 1rem, exactly like p-4.

The family builds up in tiers, from the narrowest to the broadest.

Single edge. top-*, right-*, bottom-*, left-* each set one offset. top-0 pins the element’s top edge to the containing block’s top; right-4 holds it 1rem in from the right edge. Beyond the spacing scale they also take fractions and top-full (100% of the containing block), plus arbitrary values for genuine one-offs. The detail worth dwelling on is negative offsets like -top-4 and -left-2, which pull the element outside its containing block. That’s not a hack; it’s the standard move for a badge that overhangs a card corner. -top-2 -right-2 lifts the badge up and out so it straddles the edge instead of sitting tucked inside it.

Axis pairs. inset-x-* sets left and right together; inset-y-* sets top and bottom together. inset-y-0 is the everyday way to say “stretch this from the top of the containing block to the bottom,” and you’ll see it in the side-drawer pattern in a moment.

All four at once. inset-* sets all four offsets, and the one you’ll reach for constantly is inset-0: top, right, bottom, and left all 0, which means “fill the containing block exactly.” Paired with absolute or fixed, inset-0 is the full-cover pattern. It’s how you lay a backdrop over a card or a dimming overlay over the whole screen.

Utility
CSS it emits
Effect
top-4
top: 1rem;
one edge held 1rem in from the top
-top-2
top: -0.5rem;
pulled above the containing block's top edge
inset-x-0
left: 0; right: 0;
stretched edge-to-edge across the width
inset-y-0
top: 0; bottom: 0;
stretched top-to-bottom over the height
inset-0
top: 0; right: 0; bottom: 0; left: 0;
all four offsets at 0 — fills the containing block

Logical offsets. There’s one more tier, and it’s the one to reach for by default. Physical offsets like left-* and right-* are baked to the screen’s left and right. That’s wrong the moment your app renders in a right-to-left language like Arabic or Hebrew, where “start” is the right edge, not the left. The logical forms fix that: inset-s-* and inset-e-* set the inline-start and inline-end offsets (the start and end of the text-flow direction), and inset-bs-* and inset-be-* set the block-start and block-end (top and bottom in normal writing). Under direction: rtl, inset-s-* automatically flips to the right edge, with no media query and no duplicate styles. This is the same logical-property idea you met with ps-* and pe-* for padding earlier in the chapter, now extended to offsets.

The code conventions make logical properties the default for any project that might ever ship in an RTL language: inset-s-* over left-*, every time, so RTL works for free. In an LTR-only project, physical offsets are acceptable and the migration to logical is mechanical if you ever need it, but starting logical costs you nothing and saves a rewrite later.

You now have the model (containing block) and the vocabulary (inset). What turns that into a skill is recognizing the handful of shapes that come up over and over in a real SaaS app, the ones an experienced developer types without thinking. There are about five. The gallery below renders each one live alongside its class string, so you can see the pattern and the code together and inspect either in DevTools.

Each tab shows the shape, a one-line note on when you reach for it, and the gotcha that trips people up.

New

Starter plan

Everything to launch your first project.

<article className="relative rounded-xl border p-4">
<span className="absolute -top-2 -right-2 rounded-full bg-blue-600 px-2 py-0.5 text-xs text-white">
New
</span>
<h3 className="font-semibold">Starter plan</h3>
</article>

A relative card with an absolute badge overhanging the top-right corner. This is the basic unit, rendered: the badge anchors to the card because the card is positioned.

A few gotchas from those tabs are worth pulling out, because each one is a clean pattern that quietly turns into a bug. The badge’s negative offsets only overhang if the card doesn’t clip its overflow; an overflow-hidden would shave off the half that pokes out, and you’ll meet overflow properly in the next lesson. The toast’s z-50 just means “stack above the page.” The number itself, and why it sometimes fails, is the job of the stacking-context lesson later in this chapter. For any bottom-pinned chrome on iPhones, add pb-[env(safe-area-inset-bottom)] so the toast clears the home-indicator bar, one of the rare arbitrary values that earns its keep. Finally, a count badge means nothing to a screen reader as a bare “3” in a corner, so give it an aria-label like “3 unread” to make the number reach assistive tech. The accessibility depth there belongs to a later chapter.

sticky earns its own section because it confuses more people than the other four combined, and almost always for one of three specific reasons. Once the model is clear, the confusion clears with it.

Here’s the model. A sticky element behaves exactly like a relative one, sitting in its normal slot, in flow, holding its space, until scrolling would carry it past the offset you set. The instant it would scroll past top-0, it stops and sticks at that offset, behaving like fixed. The part that separates it from fixed is that it only sticks within its parent’s bounds. Once you scroll far enough that the parent itself leaves the screen, the sticky element leaves with it. It never escapes its parent. That’s why a sticky section header stays pinned while you read that section, then slides away as the next section’s header takes over: each header is sticky within its own section’s box.

When sticky “doesn’t work,” it’s nearly always one of three preconditions missing. Here’s the checklist to walk through:

  1. No offset set. position: sticky with no top, bottom, left, or right does nothing, because it has no idea where to stick. This is the most common one. The fix is top-0, or whichever edge you want it to pin to.
  2. No scrollable ancestor. Sticky needs something to scroll, either the page itself or an ancestor that scrolls on the relevant axis. If nothing scrolls, there’s no scroll for it to react to, and it just sits there like a relative element. (Scroll containers are the next lesson’s whole topic; for now, know that sticky depends on one.)
  3. Parent too short. Sticky only sticks within its parent’s height. If the parent is barely taller than the sticky element itself, there’s no room to scroll inside it, so the element appears to do nothing. It is working; there’s just nowhere to stick to.

So when a teammate says “sticky is broken,” the move isn’t to fiddle with values. It’s to check those three: is there an offset, is there a scroll container, and is the parent tall enough? One of them is almost always the answer.

Scroll behavior is motion, and motion is hard to convey in prose. The video below shows position: sticky in action with the same Tailwind utilities you’ve been reading.

Five modes is enough to need a decision procedure, and the practical one collapses to just two questions asked in order. The walker below is that procedure; work through it the way you’d reason about a real element on a real screen. The value isn’t in any single answer at the bottom. It’s in the order of the questions, because that order is how an experienced developer narrows from “this thing needs to move” to “this is the mode” in a couple of seconds.

Which position mode?

Now it’s your turn to run into the central bug and fix it. You’re building a notification card: an avatar on the left, a title and message in the middle, and a small unread badge pinned to the card’s top-right corner, overhanging the edge slightly. This is exactly the pattern the whole lesson has been building toward. The parent needs relative, or the badge flies to the corner of the preview instead of the card.

You already know the pieces from earlier in the chapter: size-* sizes the avatar, and flex-1 min-w-0 lets the text region fill the row and truncate cleanly. The new work is the badge: absolute on it, relative on the card, and negative offsets to make it overhang.

Pin the unread badge to the card's top-right corner so it overhangs the edge slightly. The card already has the avatar and text laid out — you add the positioning. Match the target.

Target
Your output LIVE

If your badge jumped to the corner of the editor preview when you added absolute, you just reproduced the lesson’s central bug for yourself, and the one-class fix (relative on the <article>) is the habit to carry away from it.

One last bit of orientation before you go, because the ground here is shifting in a way that changes what you’ll reach for. For years, tethering a tooltip to a button or a dropdown to its trigger meant reaching for a JavaScript positioning library: code that measured the trigger, measured the viewport, and recomputed coordinates on every scroll and resize. In 2026 the browser platform is absorbing that job, and two features are driving the change.

The first is CSS Anchor Positioning. It lets an element position itself relative to any other element on the page, not just its containing-block ancestor, by naming an anchor (anchor-name) and pointing at it (position-anchor). That’s the native answer to “tether this tooltip to that button,” and it’s usable today: it reached Baseline in 2026 (Chrome 125+, Firefox 147+, Safari 26+).

The second is the Popover API: the native popover attribute on an element, plus a popovertarget button to toggle it (and a showPopover() method in JS). It gives you top-layer popovers, dropdowns, and menus with light-dismiss (click outside to close) and focus handling built in, with no positioning library and no manual z-index. It renders in the top layer , which is exactly why it sidesteps the z-index traps you’ll study later this chapter.

Here’s the takeaway, and it’s why this is orientation rather than instruction: you will almost never write these from scratch. shadcn’s Popover, Dropdown, and Tooltip components (which you’ll meet in the chapter on the shadcn component surface) wrap both features for you, and they also solve the fixed-under-transform trap by portaling to <body>. So the job here isn’t to memorize the APIs. It’s to recognize the names, know the platform is doing the heavy lifting, and reach for the wrapped component instead of hand-rolling positioning math.

Keep these open while the mental model settles into place.