To keep a background image pinned to the viewport while the content scrolls over it (the cheap parallax look), set background-attachment: fixed. The background stays put; everything else moves. That is the whole trick:
.hero {
background-image: url("/images/mountains.jpg");
background-attachment: fixed;
}background-attachment: fixed positions the image relative to the viewport instead of the element. As you scroll, the element moves but the image does not, so content appears to slide over a stationary photo. It is not real parallax (nothing moves at a different rate), but it reads as parallax for one line of CSS. The default is scroll, where the image is fixed to the element and scrolls away with it.
The full full-bleed recipe
A bare fixed rarely looks right on its own. For a hero that fills the viewport and covers cleanly at any size, add three more properties: background-size: cover so the image scales to fill without distorting, background-position: center so the crop stays sensible, and background-repeat: no-repeat so you never see a tiled seam.
.hero {
min-height: 100vh;
background-image: url("/images/mountains.jpg");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
}Or, collapsed into the background shorthand (note size comes after position, separated by a slash):
.hero {
min-height: 100vh;
background: url("/images/mountains.jpg") center/cover no-repeat fixed;
}Both are identical. The shorthand silently resets any background property you leave out to its initial value, so I keep the longhand while iterating and collapse to the shorthand once the values settle.
background-attachment is not background-position
This is the one thing the old comment threads on this topic always got wrong. Two different properties do two different jobs:
background-attachmentcontrols scroll behavior: does the image move with the element, or stay fixed to the viewport. Values:scroll,fixed,local.background-positioncontrols placement: where the image sits in its painting area (center,top left,50% 25%).
There is no background-position: fixed. Write that and the browser drops the invalid declaration, which is exactly why people who reached for it saw "nothing happened" and concluded fixed backgrounds were broken. The value you want is background-attachment: fixed: position decides where, attachment decides whether it scrolls.
The mobile caveat (this is the important one)
Here is the part that bites everyone: background-attachment: fixed does not work on iOS Safari, and is unreliable across mobile browsers generally. Safari on iPhone and iPad ignores it on purpose, not as a bug, because repainting a viewport-fixed background every scroll frame was too expensive on mobile hardware. On affected browsers the background just behaves as scroll (and historically some Android versions painted it in jumpy ways).
So a hero that looks great on desktop quietly falls back to a normal scrolling background on a phone, which is usually fine. If it is not, the standard workaround is to drop background-attachment and instead put the image on a separate element you fix with position: fixed, then layer content above it:
.hero {
position: relative;
min-height: 100vh;
}
.hero::before {
content: "";
position: fixed;
inset: 0;
z-index: -1;
background: url("/images/mountains.jpg") center/cover no-repeat;
}A real position: fixed element is honored everywhere, including iOS Safari, so the pinned-background look survives on mobile. The trade-off: it sits behind all scrolling content, so this suits a single full-page backdrop rather than several stacked fixed-background sections.
If you would rather just turn the effect off on small screens, a media query is the least-effort fix:
@media (max-width: 768px) {
.hero {
background-attachment: scroll;
}
}That gives desktop the parallax-style hero and gives mobile a plain, well-behaved background instead of relying on the browser to silently do the right thing.
A note on performance and motion
Even where supported, fixed forces the browser to repaint the background as you scroll, which costs frames on large images or busy pages. Keep the source reasonably sized and let cover do the scaling. If you build genuine multi-speed parallax with JavaScript or transforms on top, respect users who asked for less motion:
@media (prefers-reduced-motion: reduce) {
.hero {
background-attachment: scroll;
}
}The fixed-background look is harmless enough that I do not always gate it behind prefers-reduced-motion, but any layered, multi-speed parallax should be.
FAQ
See also
- Fixed vs sticky positioning in CSS: when to reach for
position: fixed(the mobile workaround above) versusposition: sticky, and how each behaves on scroll. - CSS filters: darken or blur a hero background with
filteror a::beforeoverlay so overlaid text stays readable. - The CSS :has() selector: style a section based on what it contains, handy for toggling a fixed-background treatment by content.
Sources
Authoritative references this article was fact-checked against.
- background-attachment, MDN referencedeveloper.mozilla.org
- background-position, MDN referencedeveloper.mozilla.org
- background shorthand, MDN referencedeveloper.mozilla.org





