Skip to main content
ASoc
Tutorial

Tailwind Position: 100 Instances, and the One Where relative Is Scoped on Purpose

44 absolute, 37 relative, 14 fixed instances audited — plus the scoped relative wrapper that stops a stretched card link from swallowing a neighboring button.

The ASoc Team9 min read

relative sets an element's CSS position: relative — it stays in normal document flow, but becomes the positioning anchor for any absolute descendant. This codebase uses it 37 times, almost always for exactly that reason: not to move the element itself, but to scope where something inside it is allowed to land. Across src/components and src/app, the five position utilities appear 100 times combined, and the split between them says something specific about what each one is actually for.

The short answer

relative positions an element in normal flow while making it the anchor for absolute children — use it on the parent, not the thing you're actually moving. absolute removes an element from flow and positions it against the nearest relative (or fixed/sticky) ancestor — use it for overlays, badges, and stretched hit areas. fixed anchors to the viewport regardless of scrolling — headers, modals, off-canvas panels. sticky toggles between flow and fixed at a scroll threshold. static is the default, and in this codebase it appears only to cancel a fixed set at a smaller breakpoint.

The census

100 total position instances

 44  absolute     37  relative     14  fixed     4  static     1  sticky

absolute outnumbering relative by seven is the first thing worth noticing — not every absolute element needs its own dedicated relative wrapper nearby, because several absolute children can share one relative ancestor (a card's cover art holds three: a fill image, a hover overlay, and a corner badge cluster, off two relative parents total). And sticky at a single instance says this site has almost no scroll-anchored UI — the one place is /docs, not the marketing pages.

relative + absolute: not always the pair you'd guess

The obvious pairing is "wrap the thing, then absolutely position inside it" — and the least obvious part of this codebase's use of it is how tightly scoped the relative wrapper is kept. TemplateCard's stretched title link is the clearest example:

// src/components/molecules/TemplateCard.tsx
{/* `relative` anchors the stretched title link here, so it covers the
    text block only and leaves the cover art to its own link + overlay. */}
<div className="relative mt-5 px-2 text-left">
  <h2 className="text-lg font-medium text-title-color dark:text-white/90">
    <a href={`/templates/${product.slug}`}>
      {product.name}
      <span className="absolute inset-0"></span>
    </a>
  </h2>
  {/* ... */}
</div>

The empty <span className="absolute inset-0"> is the "stretched link" pattern: it expands the anchor's clickable area to fill its relative ancestor, so clicking anywhere in that text block navigates, without wrapping the whole card (cover art, preview button, wishlist heart) in one giant <a> — which would nest interactive elements inside a link and break every one of them for assistive tech and for onClick handlers that need their own click target.

The relative here is deliberately scoped to mt-5 px-2 text-left — the text block alone — not the card. A few lines up, the same card's cover-art wrapper carries its own separate absolute cluster for a completely different job:

{/* Inside the media group (same pixel spot as the card's own padding
    would give) so hovering the heart doesn't drop the overlay; z-20
    keeps it above that overlay. */}
<div className="absolute top-2 right-2 z-20 flex items-center gap-2">
  {showDownloads && <DownloadMenu options={downloadOptions} productName={product.name} variant="icon" />}
  <WishlistButton entry={wishlistEntry} />
</div>

Two absolute elements, two separate relative scopes, one card. Merging them into a single relative wrapper around the entire card would work visually — CSS doesn't care — but it would blur the ownership: a future edit to the cover art's overlay could accidentally reposition the title's hit area, because both would be measuring against the same box instead of the one each actually belongs to.

fixed: the viewport, not the page

Fourteen instances, and every one of them is UI that has to stay put while the page under it scrolls — a header, a modal, an off-canvas panel:

// src/components/organisms/Header.tsx
<header className="fixed top-0 left-0 z-9999 w-full bg-white shadow-sm dark:bg-gray-900">

The mobile nav drawer in the same file does something sharper — it's fixed on mobile and explicitly not fixed on desktop, in one class list:

className={`fixed inset-y-0 right-0 z-9999 flex w-[85%] max-w-xs flex-col ...
  xl:static xl:inset-auto xl:z-auto xl:max-w-none xl:flex-row ...`}

Below xl, this panel is a fixed slide-in drawer pinned to the right edge of the viewport. At xl and up it becomes the desktop nav row: xl:static cancels the fixed positioning entirely, dropping the element back into normal document flow so it can lay out horizontally as part of the header instead of floating over the page. One element, two entirely different position values, chosen by breakpoint rather than by two separate components.

The skip-to-content link at the top of every page goes further still — it's fixed only conditionally, on focus:

// src/app/layout.tsx
className="sr-only rounded-lg font-medium focus:not-sr-only focus:fixed
  focus:top-4 focus:left-4 focus:z-[10001] focus:bg-primary focus:px-4
  focus:py-2 focus:text-sm focus:text-white focus:shadow-lg"

sr-only visually hides it while leaving it in the DOM (and the tab order) for screen readers. The moment a keyboard user tabs to it, focus:fixed pulls it out of flow and pins it to the top-left corner, visible for exactly as long as it's focused. position: fixed set by a pseudo-class rather than unconditionally is the detail that's easy to miss reading the utility in isolation.

z- and fixed travel together, and the stack has a shape

Every fixed element here also carries a z- value, because fixed elements from different components inevitably end up stacked and the browser needs a tiebreaker. Reading them in order: the header sits at z-9999, the mobile nav backdrop at z-9998 (behind the header, so the header stays visible above its own dimmed page), and every modal-scale overlay — PreviewModal, SavedTemplates's side-sheet — reaches for z-[10000] or z-[10001], one full order of magnitude above the header. That gap isn't arbitrary: it guarantees a modal always wins over navigation chrome without anyone having to check what number the header currently uses.

sticky: one instance, and it's the only scroll-anchored UI on the site

// src/components/organisms/DocsSidebar.tsx
<aside className="shrink-0 lg:sticky lg:top-24 lg:h-[calc(100vh-7rem)] lg:w-64 lg:overflow-y-auto lg:pr-2">

sticky behaves like relative until the element would scroll past top-24 (6rem from the viewport top, clearing the fixed header), at which point it locks there like fixed — but only within its own parent's box, unlike fixed's viewport-wide anchor. lg:h-[calc(100vh-7rem)] caps its height to the remaining viewport so a long nav tree scrolls internally instead of pushing the page down. This is the only place on the site with content long enough, and a layout wide enough, to make a sticky sidebar worth the complexity — the marketing pages and product pages have no comparable secondary nav to anchor.

Comparison: which position value for which job

UtilityCSS valueStays anchored toUsed here forCount
relativeposition: relativeIts own normal-flow spotScoping an absolute child's coordinate system37
absoluteposition: absoluteNearest positioned ancestorOverlays, corner badges, stretched hit areas44
fixedposition: fixedThe viewportHeader, modals, off-canvas panels, the skip link14
stickyposition: stickyFlow, then the viewport past a thresholdThe one long secondary nav on the site1
staticposition: static (default)Nothing — normal flowCancelling fixed at a larger breakpoint4

Mistakes and how they show up

SymptomCauseFix
An absolute element anchors to the whole page instead of its intended boxNo relative (or fixed/sticky) ancestor between it and <html>Add relative to the nearest real parent, scoped as tightly as the element it's positioning
A stretched link swallows clicks meant for a sibling buttonrelative wraps a box that also contains the button, so the absolute inset-0 span covers itScope relative to just the linked text, as TemplateCard does, not the whole card
Two fixed overlays fight for the top layerMissing or equal z- valuesReserve a numeric band per UI tier (nav below modals) rather than reusing one z-50 everywhere
A sticky element never sticksAn ancestor has overflow: hidden or overflow: auto, which breaks sticky's containing-block chainRemove the overflow on the ancestor, or move the sticky element outside it
A responsive drawer stays fixed on desktop when it shouldn'tNo breakpoint variant resetting positionAdd xl:static (or the equivalent) exactly as the mobile nav drawer does
top-4 left-4 does nothing on an absolute/fixed elementThe offsets were set without also setting a position value on the same elementtop-*/right-*/bottom-*/left-* are inert without relative, absolute, fixed, or sticky first

Frequently asked questions

What's the difference between relative and absolute in Tailwind? relative keeps the element in normal document flow and makes it a positioning anchor for descendants. absolute removes the element from flow entirely and positions it against the nearest ancestor that has relative, absolute, fixed, or sticky set — falling back to the viewport if there is none, which is usually the bug, not the intent.

Do you need relative on the immediate parent, or any ancestor? Any ancestor — absolute looks up the tree for the nearest positioned one, not specifically the direct parent. That's exactly why TemplateCard can keep its cover-art absolute badges and its title's absolute stretched link scoped to two different, narrower relative boxes instead of one wrapping the whole card.

When should something be fixed instead of sticky? fixed when it should never move regardless of scroll position or which section of the page you're in — a header, a modal. sticky when it should scroll normally until a threshold, then hold — a sidebar nav that shouldn't cover content before the reader reaches it.

Why does position: static ever need to be written explicitly, since it's the default? To cancel a position value set at a smaller breakpoint. This codebase's only four static instances are all xl:static, undoing the fixed positioning the mobile nav drawer needs below that breakpoint — without it, the desktop nav row would still be floating over the page instead of sitting in the header's flex row.

Templates in this post

ASoc Remit markets a payments platform around a transactions-dashboard preview and a three-tier comparison pricing table, ASoc Script is an AI copywriting SaaS site with an interactive generator hero and a filterable template gallery, and ASoc Seeker markets an AI keyword-research tool with a topic-to-keywords hero and a two-tier pricing table — all built on the same relative/absolute/fixed conventions audited above.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Tutorial11 min read

Scroll-Driven Animations in Tailwind v4 Without a JS Library

animation-timeline replaces the scroll-animation library category — behind two guards. The Tailwind v4 setup, the element you must never fade in, and when IntersectionObserver still wins.

Read more
Tutorial11 min read

Multi-Tenant Theming with Tailwind CSS v4 and CSS Variables

Tailwind v4 tokens compile to real CSS custom properties, so one build can serve every tenant's brand. The override pattern, contrast handling, and the pitfalls.

Read more