Starter plan
Everything to launch your first project.
Tailwind's position and inset utilities, used to take elements like badges, sticky headers, and toasts out of normal flow and pin them where you want.
You’re building a dashboard and the design calls for a small “Saved” toast that floats in the bottom-right corner of the screen, above everything, no matter where the user has scrolled.
You reach for what you know: flex, grid, a bit of margin.
None of it works.
margin: auto won’t push the toast down, 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 and stay fixed to the screen.
The tools you have so far aren’t built for that.
The answer is three utilities: fixed bottom-4 right-4 z-50.
That string pins a toast to the bottom-right of the viewport.
Two parts of it are worth a question: why fixed and not absolute, and what is z-50 doing?
By the end of this lesson you’ll know all five position modes, what each one anchors against, and how to build badges, sticky headers, toasts, and drawers without landing in the wrong corner.
First, one connection.
Every element you’ve laid out so far, flex item, grid item, or stacked block, has lived in normal flow : flow is what makes a paragraph push the next one down and a flex item leave room for its neighbors.
position is how a small set of elements opt out of flow.
That set stays small, most of your layout is still flex and grid, and you reach for position only for the few elements that genuinely need to escape: a badge on a corner, a header that sticks while you scroll, a toast pinned to the screen.
The position property has five values, and the way to keep them straight is to ask one question of each: what is this element positioned against?
Once you know what each is measured from, the offsets (top, left, and the rest) stop being mysterious.
static is the default. The element goes exactly where normal flow puts it, and offsets don’t apply. Everything you’ve styled so far has been static; you just never had to say so.relative stays in flow, so its slot is preserved and its siblings don’t move, but offsets shift it visually from that slot. Its main job in 2026 isn’t that nudge, though: it’s to become an anchor for absolute children, 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 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, 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.
The most useful split across these five is whether the element is in flow or out of flow .
static, relative, and sticky (before it sticks) hold their slot; absolute and fixed reserve nothing, and the layout closes up around them.
Get this solid first, because it explains why dropping an absolute element into a row doesn’t shove the 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.
staticrelativestickyabsolutefixedThe one that trips people up is sticky.
It belongs in the stays column because it holds its normal slot right up until the scroll threshold, and only then behaves like fixed.
Definitions only get you so far with position; the point is what happens to a box’s neighbors the instant you change its mode.
In the playground below, one highlighted box sits among three plain siblings inside a bordered parent.
The select changes the highlighted box’s position, and the sliders set its top and left offsets.
Switch modes and watch the siblings:
static, the box sits in its normal slot and the offsets do nothing. This is the default, which is why position never came up before now.relative, the box keeps its slot, so the siblings stay put, but the offsets shift it visually. It leaves a gap behind: its original space is still reserved.absolute, the siblings snap together to fill the hole. The box is out of flow, so it reserves nothing, and the offsets now measure from the parent’s corner instead of its old slot. (Why the parent’s corner? That’s the next section.)relative leaves a hole and absolute closes it: that is the in-flow/out-of-flow split made visible.
target
sibling
sibling
sibling
When you give an element position: absolute and set top and left, the browser measures those offsets from its containing block .
For an absolute element, that block is the nearest ancestor that is itself positioned: the nearest one with position set to relative, absolute, fixed, or sticky.
The browser walks up the tree parent by parent, and the first positioned ancestor it finds becomes the containing block.
If it reaches the top without finding one, it falls back to the initial containing block , which is near enough the whole viewport.
That fallback is the most common positioning bug in real code, and you will hit it.
Picture 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.
The reason follows straight from the rule: nothing in the card is positioned, so the badge’s walk up the tree finds no anchor and stops at the viewport.
The fix is one class, relative on the card, which gives the badge a positioned ancestor to find before it climbs any higher.
<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.
<article className="relative 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>One class fixes it. relative on the card gives the badge a positioned ancestor to stop at, so top-2 right-2 now measure from the card’s own corner.
Make this pairing automatic, because you will type it constantly: a relative parent plus an absolute child is the basic unit of corner-positioning.
Whenever you 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 only job is to be the anchor.
The diagram below makes the walk-up literal.
One exception belongs to a later lesson, but it is worth naming now so you recognize it.
A fixed element normally anchors to the viewport, ignoring its ancestors.
The exception: if an ancestor has transform, filter, or perspective set, that ancestor silently becomes the containing block, so your fixed modal ends up positioned against some transformed parent halfway down the page.
The fix is to portal the element out to <body> so it has no such ancestor.
For now, carry forward one thing: a transform on an ancestor can capture a fixed child.
You know what an element is positioned against; now 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 draw from the same --spacing scale as padding and margin, 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-*, and 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.
They also take fractions, top-full (100% of the containing block), and arbitrary values for one-offs.
Negative offsets like -top-4 and -left-2 pull the element outside its containing block, 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.
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.”
All four at once. inset-* sets all four offsets.
The one you’ll reach for is inset-0: top, right, bottom, and left all 0, which fills the containing block exactly.
Paired with absolute or fixed, inset-0 is the full-cover pattern: a backdrop over a card, or a dimming overlay over the whole screen.
Logical offsets. This last tier is the one to reach for by default.
Physical offsets like left-* and right-* are baked to the screen’s left and right, which is wrong the moment your app renders in a right-to-left language like Arabic or Hebrew, where “start” is the right edge.
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-* flips to the right edge with no media query and no duplicate styles, the same logical-property idea as the ps-* and pe-* padding utilities earlier in the chapter.
So the rule is inset-s-* over left-*, every time, so RTL works for free.
In an LTR-only project physical offsets are acceptable and the migration is mechanical, but starting logical costs you nothing and saves a rewrite later.
With the containing block as your model and inset as your vocabulary, what’s left is recognizing the handful of shapes that recur across a real app.
There are about five.
The gallery below renders each one live next to its class string, so you can read the pattern and inspect it in DevTools at the same time.
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. The badge anchors to the card because the card is positioned.
<h2 className="sticky top-0 bg-white py-2 font-semibold"> Recent activity</h2>A sticky top-0 heading that pins to the top of its scroll container as the section scrolls past. Scroll the panel to see it. It needs both an offset (top-0) and a scroll container.
<div className="fixed right-4 bottom-4 z-50 rounded-lg bg-slate-900 px-4 py-2 text-sm text-white shadow-lg"> Saved</div>fixed bottom-4 right-4 z-50 pins the toast to the viewport corner, above page content. It’s framed in a fake window here so it stays on the page.
<aside className="fixed inset-y-0 right-0 w-72 border-l bg-white p-4"> {/* settings */}</aside>fixed inset-y-0 right-0 w-72 is a full-height panel pinned to one edge: inset-y-0 stretches it top-to-bottom, w-72 sets the width, right-0 pins the edge. It’s framed here so you can see it.
<div className="absolute inset-0 bg-black/50" />absolute inset-0 fills its parent; fixed inset-0 fills the screen as a modal backdrop. Here it covers the card; over the whole screen it’s the dimming layer behind a modal.
Four gotchas from those tabs each turn quietly 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 part that pokes out (overflow comes in the next lesson).
The toast’s z-50 means “stack above the page”; the number, and why it sometimes fails anyway, is the stacking-context lesson later in this chapter.
For bottom-pinned chrome on iPhones, add pb-[env(safe-area-inset-bottom)] so the toast clears the home-indicator bar.
And a count badge reads as a bare “3” to a screen reader, so give it an aria-label like “3 unread” (more on accessibility in a later chapter).
sticky confuses people more than the other four positions combined, almost always for one of three reasons.
Get the model right and the confusion clears.
A sticky element behaves like a relative one, sitting in its normal slot and holding its space, until scrolling would carry it past the offset you set.
The instant it would scroll past top-0, it pins at that offset like fixed, with one difference: it only sticks within its parent’s bounds.
Scroll far enough that the parent leaves the screen and the sticky element leaves with it.
That’s why a sticky section header stays pinned while you read its section, then slides away as the next header takes over, each one sticky within its own section’s box.
When sticky “doesn’t work,” one of three preconditions is missing:
position: sticky with no top, bottom, left, or right does nothing, because it has no idea where to stick. This is the most common case. The fix is top-0, or whichever edge you want to pin to.relative element. (Scroll containers are the next lesson’s topic; for now, know that sticky depends on one.)Scroll behavior is motion, and motion is hard to show in prose.
The video below puts position: sticky in action with the same utilities you’ve been reading.
Five modes need a decision procedure, and the practical one is two questions asked in order. Work the walker below the way you’d reason about a real element on a screen. What matters isn’t the verdict at the bottom but the order of the questions: that order is how you narrow from “this needs to move” to “this is the mode” in seconds.
Opens a containing block so absolute children anchor to it. No offsets needed; its whole job is to be the anchor.
You don’t set position at all. This is the default every element already has.
fixed plus inset offsets plus z-50. Watch for a transform/filter ancestor that captures it; portal out to <body> if so.
sticky top-0. Needs an offset and a scroll container, and the parent has to be taller than the element.
absolute inside a relative parent. Use inset-0 to fill the parent, or single edges (with negative offsets) to pin a corner.
Now build a notification card: an avatar on the left, a title and message in the middle, and a small unread badge pinned to the top-right corner, overhanging the edge.
The badge needs absolute; without relative on the card, it anchors to the preview instead.
You know the rest 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.
If the badge jumped to the corner of the preview when you added absolute, that’s the bug; relative on the <article> is the one-class fix to remember.
Tethering a tooltip to a button or a dropdown to its trigger used to mean a JavaScript positioning library that measured everything and recomputed coordinates on every scroll.
The browser now does this natively: CSS Anchor Positioning lets one element position itself relative to any other on the page, and the Popover API gives you popovers, dropdowns, and menus with light-dismiss and focus handling built in.
Both reached Baseline in 2026.
Popovers render in the top layer , which sidesteps the z-index traps you’ll study later this chapter.
You’ll almost never write either from scratch. shadcn’s Popover, Dropdown, and Tooltip components wrap both features for you, so the job is to recognize the names and reach for the wrapped component instead of hand-rolling positioning math.
The native way to tether one element to another — anchor-name, position-anchor, and the fallback machinery.
The popover attribute and popovertarget button — top-layer popovers with light-dismiss, no z-index wrangling.