skip to content

Why does an element declared position: sticky with no top, right, bottom, or left value never stick?

level: middleimportance: must knowfreq 60%

answer

  1. the value carries the rule, not the keyword
  2. auto is the initial inset value
  3. a floor, not a shift
  4. margins do not create thresholds
  5. with no constraint it is just relative

basics

~20 s

An inset value is what defines the threshold a sticky box must hold. With every inset left at its default of auto there is no constraint to enforce, so the box stays at its normal-flow position and behaves exactly like position: relative.

solid answer

~50 s

Sticky positioning is a constraint, not a mode. The declaration `position: sticky` only says which algorithm applies; the inset properties say what it must enforce. `top: 0` means "this box's top edge may never sit more than 0 from the top edge of its scrollport" — that line is the threshold, and the browser offsets the box only when scrolling would push it past. If `top`, `right`, `bottom` and `left` are all `auto`, which is their initial value, there is no line to hold, so nothing is ever offset and the element renders in normal flow forever. That is why the usual bug report is "sticky does nothing" rather than an error: the styles are valid and applied, they just declare no constraint. A margin is not a substitute — only insets create the threshold.

code

css · 11 lines
css
/* Does nothing: valid, applied, and constraint-free */
.does-nothing {
  position: sticky;
  margin-top: 1rem;
}

/* Sticks 1rem below the top edge of the scrollport */
.works {
  position: sticky;
  top: 1rem;
}

go deeper

for a junior

Remember that position: sticky always needs a companion top, right, bottom or left value, and that a margin will not do the job.

for a middle

Explain why: insets default to auto, and a sticky inset expresses a constraint against the scrollport edge rather than an offset from the flow position, so with no inset there is nothing to enforce.

for a senior

Show how you would isolate it fast in a real stylesheet — read computed insets rather than authored ones, and check for the mobile-first pattern where the inset lives in an unmatched media query or a reset sets it back to auto.

for a principal

Treat it as an API-design lesson for your own conventions: a sticky utility class that ships position without an inset is a footgun. Prefer one class that sets both, parameterised by a header-height custom property, over two classes callers must remember to pair.

## The declaration and the constraint are two different things Most CSS positioning values do something on their own. `position: fixed` with no offsets still leaves the flow. `position: absolute` with no offsets still leaves the flow and sits at its static position. `position: sticky` is the odd one out: on its own it changes almost nothing you can see. It selects an algorithm, and that algorithm needs a parameter before it has any work to do. That parameter is the inset: `top`, `right`, `bottom`, `left`, or the logical equivalents `inset-block-start`, `inset-inline-start` and friends, or the `inset` shorthand. Each one you declare adds one constraint the box must satisfy relative to its scrollport. ## Reading an inset as a rule, not an offset On a relatively positioned box, `top: 20px` means "paint this 20px lower than where flow put it". On a sticky box it means something else entirely: "the top edge of this box must never be closer than 20px to the top of the scrollport". It is a floor, not a shift. While the box naturally sits below that line, the rule is already satisfied and the browser does nothing at all. Only when scrolling would carry the box above the line does the browser offset it — by exactly as much as it needs, and no more — to keep the rule true. ```css .filter-bar { position: sticky; top: 0; /* engages when the bar reaches the top of the scrollport */ } .footer-cta { position: sticky; bottom: 0; /* engages from below: pins while its container is still on screen */ } ``` That framing explains the behaviour people find surprising: a sticky element does not jump when it engages, because at the instant it engages the required offset is zero and it grows continuously from there. ## Why `auto` means "never" The initial value of all four inset properties is `auto`. For a sticky box, `auto` means the box is not constrained on that side at all. If every side is `auto`, the box has no constraints, so the sticky algorithm never has anything to enforce, and the box is laid out and painted at its normal-flow position for the entire scroll. Visually and structurally that is identical to `position: relative` with no offsets — in flow, space reserved, painted where flow put it. The browser has nothing to warn you about, which is what makes this a debugging question rather than a syntax question. DevTools shows `position: sticky` applied and winning the cascade; the element simply has no threshold. ## The near-misses that produce the same symptom Several real mistakes reduce to the same "no inset in effect" state: - **Using a margin instead of an inset.** `margin-top: 1rem` moves the box in flow; it never creates a sticky threshold. If you want a stuck element to sit 1rem below the top of the scrollport, that is `top: 1rem`. - **Declaring the inset only in a media query.** Common with mobile-first stylesheets: `position: sticky` is unconditional, `top: 0` lives inside a breakpoint. Below that breakpoint the element quietly does nothing. - **Sticking on the wrong axis.** `left: 0` constrains horizontal scrolling only. In a vertically scrolling page it is a valid declaration that will never engage, which is a favourite in sticky-first-column table layouts where people set the wrong side. - **Having the inset overridden.** A later rule setting `top: auto` — often a reset or a shared utility — removes the constraint while leaving `position: sticky` in place. ## Picking the right side The side you declare determines which edge of the scrollport the box holds and therefore which direction it engages from. `top` pins as content scrolls up past it, the usual choice for headers. `bottom` pins from the other end and is how you keep an action bar visible at the bottom of a container while that container is still in view. You can declare more than one — `top: 0` together with `left: 0` is exactly how the corner cell of a table with both a sticky header row and a sticky first column is made to hold both edges. Declaring opposite sides on the same axis is legal but rarely useful: the box is constrained at both ends of the scrollport, so it only floats free in between.

  • If a sticky element has top: 20px, why is there no visual jump of 20px when it engages?
    Because the inset is a constraint, not an offset applied on engagement. While the box sits lower than 20px from the scrollport top the rule already holds and no offset is applied. At the exact scroll position where it would breach, the required offset is zero, and it then grows continuously as you keep scrolling. The transition is seamless by construction.
  • What happens if you set both top and bottom on the same sticky element?
    Both constraints apply, so the box is held inside a band: it cannot go above the top threshold or below the bottom one, and it moves freely in between. It is legal but rarely what people want. The one common multi-inset case is combining different axes — `top` plus `left` — so a box holds both a horizontal and a vertical edge.
  • How would you spot this bug in DevTools, given the styles look correct?
    Look at the computed values of `top`, `right`, `bottom` and `left` rather than at `position`. If all four compute to `auto`, the element has no threshold, and that is the whole bug. It is also worth checking whether the inset rule is inside a media query that does not currently match, or has been reset to `auto` by a later rule.

saying these in an interview costs you the question

  • Thinks position: sticky alone is enough to pin an element
  • Uses margin-top expecting it to create the threshold
  • Says the inset shifts the element by that amount when it sticks
  • Assumes the browser defaults the inset to zero
  • Sets left: 0 for a vertically scrolling sticky header

context