You notice it the moment you scroll: the background image either “sticks” to the page like wallpaper, or it moves with the content like it’s painted on the element. That tiny difference can make a landing page feel calm and grounded—or jittery and distracting. I’ve seen teams spend hours tweaking parallax libraries when the right answer was a single CSS declaration (and, just as often, I’ve seen a single CSS declaration cause mobile bugs that only show up on the CEO’s phone).
background-attachment is one of those CSS properties that looks simple on paper and then gets complicated in real layouts: nested scroll containers, multiple background layers, performance constraints, and mobile browser quirks. If you build UI for marketing pages, docs sites, dashboards, or any app with scrollable panels, you’ll run into it.
Here’s what you’ll walk away with: a clear mental model of scroll, fixed, and local; runnable examples you can paste into a file and test; and the practical rules I follow in 2026 to decide when to use it, when to avoid it, and how to ship it safely.
What background-attachment Actually Controls
The background-attachment property decides what the background image is “attached” to while scrolling. I explain it to teammates with a simple analogy:
scroll: the background is taped to the element’s box. When the page scrolls, the element moves, so the background moves with it.fixed: the background is taped to the viewport (your browser window). Scrolling moves the content over a stationary background.local: the background is taped to the element’s scrollable content area. If the element itself scrolls (thinkoverflow: auto), the background scrolls inside that element.
The core syntax looks like this:
background-attachment: scroll
local
inherit;
A few clarifications that prevent confusion later:
background-attachmentapplies per background layer. If you specify multiple background images, you can set different attachments for each.- It affects background images (and gradients). A plain background color doesn’t “scroll,” so attachment doesn’t change much for colors alone.
- It interacts with how the background is painted (size/position/repeat/origin/clip). Attachment is one piece of the background puzzle.
Building the Mental Model: scroll vs fixed vs local
When someone reports “the background is moving wrong,” I ask one question: “Which scroll are you talking about?” There are two common scroll contexts:
1) Page/viewport scroll (the classic document scroll)
2) Element scroll (a container with its own scrollbars)
Now map each value to those contexts:
scroll (default)
With scroll, the background moves with the element as the page scrolls. If you never set background-attachment, this is what you get.
What it feels like: the background is printed on the element.
Important nuance: the background does not move when the element’s contents scroll inside an overflow: auto container (because scroll is not tied to the content scroll). It’s tied to the element’s box.
fixed
With fixed, the background is positioned relative to the viewport, not the element. This is the classic “parallax-ish” effect people reach for.
What it feels like: the content slides over a stationary mural.
This can look great for hero sections, but it has trade-offs:
- Some mobile browsers historically treat
background-attachment: fixedasscrollin many cases. - It can trigger more expensive painting/compositing work depending on the browser and layout.
local
With local, the background scrolls with the element’s scrollable content. This is the one many devs forget exists, and it’s extremely useful in product UIs.
What it feels like: the background is painted on the content itself.
But local only really shows its behavior when:
- The element is scrollable (
overflow: autooroverflow: scroll), and - There is enough content to scroll.
Runnable Demos You Can Paste Into a File
I’m going to keep these examples “copy into background-attachment.html and open in a browser” runnable. You’ll see the differences immediately.
Demo 1: background-attachment: scroll (baseline)
This is the default behavior. I still like setting it explicitly when teaching or when I want to prevent surprises caused by inherited styles.
:root {
–page-bg: #0b1020;
–card-bg: rgba(255, 255, 255, 0.08);
–text: rgba(255, 255, 255, 0.92);
}
body {
margin: 0;
font: 16px/1.6 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
color: var(–text);
background: radial-gradient(1200px 800px at 20% 10%, #22305f, transparent 60%),
radial-gradient(900px 600px at 85% 30%, #4b2b4d, transparent 55%),
var(–page-bg);
}
header {
padding: 56px 20px 10px;
max-width: 980px;
margin: 0 auto;
}
.panel {
max-width: 980px;
margin: 20px auto 80px;
padding: 22px;
border-radius: 18px;
background-color: var(–card-bg);
border: 1px solid rgba(255, 255, 255, 0.14);
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.22);
/ Background layer you can track while scrolling /
background-image:
radial-gradient(circle at 20% 30%, rgba(255, 255, 255, 0.16), transparent 40%),
linear-gradient(135deg, rgba(255, 255, 255, 0.10), transparent 60%);
background-repeat: no-repeat;
background-size: 520px 520px, cover;
background-position: 0% 0%, center;
background-attachment: scroll;
}
h1 { font-size: 34px; margin: 0 0 10px; }
h2 { font-size: 18px; margin: 0 0 14px; opacity: 0.9; }
background-attachment: scroll
Scroll the page: the background moves with the element.
This panel has a decorative background. With scroll, it stays aligned
to the element and moves as the element moves.
Add enough content to make the page scroll and watch the background patterns “ride along”
with the panel.
Repeat a few paragraphs so you can see the motion clearly.
When someone says “my background is scrolling,” this is usually what they mean.
Keep going…
Keep going…
Keep going…
Keep going…
Demo 2: background-attachment: fixed (viewport-tied)
This is the “stationary mural” effect. Use it intentionally; don’t sprinkle it everywhere.
body {
margin: 0;
font: 16px/1.65 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
color: rgba(255, 255, 255, 0.92);
background: #070a12;
}
.hero {
min-height: 70vh;
padding: 72px 20px;
display: grid;
place-items: center;
text-align: center;
/ A background designed to show the effect while scrolling /
background-image:
radial-gradient(900px 500px at 20% 30%, rgba(255, 255, 255, 0.20), transparent 60%),
radial-gradient(700px 420px at 80% 20%, rgba(0, 210, 255, 0.25), transparent 55%),
linear-gradient(180deg, rgba(25, 40, 85, 0.85), rgba(0, 0, 0, 0.80));
background-repeat: no-repeat;
background-size: cover;
background-position: center;
background-attachment: fixed;
}
.hero h1 {
font-size: clamp(32px, 4vw, 54px);
margin: 0 0 10px;
letter-spacing: -0.02em;
}
.hero p {
max-width: 70ch;
margin: 0 auto;
opacity: 0.92;
}
.content {
max-width: 980px;
margin: 0 auto;
padding: 28px 20px 90px;
}
.card {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
border-radius: 16px;
padding: 18px;
margin: 18px 0;
}
/ Accessibility: reduce motion by removing the viewport-tied effect /
@media (prefers-reduced-motion: reduce) {
.hero {
background-attachment: scroll;
}
}
background-attachment: fixed
Scroll down. The background stays pinned to the viewport, while the content moves.
If you prefer reduced motion, your system setting should switch this back to scroll.
scroll, that may be a browser limitation.Demo 3: background-attachment: local inside a scroll container
This is the one I reach for in dashboards: scrollable panels where the background should move with the panel’s content.
body {
margin: 0;
padding: 32px 18px;
font: 16px/1.6 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
background: #0a0f1f;
color: rgba(255, 255, 255, 0.92);
}
.layout {
max-width: 980px;
margin: 0 auto;
display: grid;
gap: 16px;
grid-template-columns: 1.1fr 0.9fr;
}
@media (max-width: 860px) {
.layout { grid-template-columns: 1fr; }
}
.panel {
border-radius: 16px;
border: 1px solid rgba(255, 255, 255, 0.14);
background-color: rgba(255, 255, 255, 0.06);
padding: 14px;
}
.scrollbox {
height: 360px;
overflow: auto;
border-radius: 12px;
border: 1px solid rgba(255, 255, 255, 0.12);
/ This background is intentionally “busy” so you can see local scrolling /
background-image:
radial-gradient(200px 200px at 15% 20%, rgba(255, 255, 255, 0.18), transparent 60%),
radial-gradient(220px 180px at 80% 40%, rgba(0, 220, 180, 0.20), transparent 60%),
linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.02));
background-repeat: no-repeat;
background-size: 500px 500px, 500px 500px, cover;
background-position: 0% 0%, 100% 10%, center;
background-attachment: local;
padding: 16px;
}
h1 { margin: 0 0 8px; font-size: 28px; }
p { margin: 0 0 10px; opacity: 0.92; }
.item {
padding: 12px;
margin: 10px 0;
border-radius: 12px;
background: rgba(0, 0, 0, 0.20);
border: 1px solid rgba(255, 255, 255, 0.10);
}
code { color: rgba(255, 255, 255, 0.92); }
background-attachment: local
Scroll inside the box (not the page). The background should move with the scrollable content.
overflow: auto).local, the background scrolls along with these items.background-attachment to scroll and compare.Multiple Background Layers: One Element, Different Attachments
Modern UIs often stack backgrounds: a subtle texture, a gradient overlay for contrast, and maybe a decorative SVG. You can set background-attachment per layer by using comma-separated lists.
Here’s a pattern I use when I want a viewport-tied layer plus an element-tied overlay (for readability). This is also a good way to keep text contrast stable.
body {
margin: 0;
font: 16px/1.65 ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial;
background: #080b14;
color: rgba(255, 255, 255, 0.92);
}
.section {
min-height: 100vh;
padding: 56px 20px;
display: grid;
place-items: center;
/ Layer 1: viewport-tied “mural” /
/ Layer 2: element-tied overlay gradient that moves normally /
background-image:
radial-gradient(900px 520px at 30% 20%, rgba(0, 200, 255, 0.25), transparent 60%),
linear-gradient(180deg, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.70));
background-size: cover, cover;
background-position: center, center;
background-repeat: no-repeat, no-repeat;
background-attachment: fixed, scroll;
}
.card {
max-width: 72ch;
border-radius: 18px;
padding: 20px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.14);
box-shadow: 0 14px 40px rgba(0, 0, 0, 0.25);
}
@media (prefers-reduced-motion: reduce) {
.section {
background-attachment: scroll, scroll;
}
}
Layered backgrounds
The bright “mural” layer stays pinned to the viewport, but the darker overlay scrolls normally.
In practice, this keeps readability consistent while still giving you the fixed-background vibe.
Try changing the second layer’s attachment to fixed and notice how the overlay itself
becomes part of the parallax effect.
If you’re testing on a phone and the first layer behaves like scroll, treat that as expected
and design your layout so it still looks good.
Second section
A second full-height section makes the effect easier to see, because you’re not only scrolling within one block.
How background-attachment Interacts With the Rest of the Background System
background-attachment is rarely the only background declaration you’re using. In real components, it’s usually paired with background-size, background-position, and sometimes background-origin / background-clip. If your background looks “wrong,” the issue is often one of these interactions rather than attachment alone.
Here’s the way I keep it straight:
background-attachmentanswers: “What scroll context is this background tied to?”background-positionanswers: “Where is the image anchored inside that context?”background-sizeanswers: “How big is the painted image?”background-repeatanswers: “Does it tile?”background-originanswers: “What box is used as the positioning area?” (border-box vs padding-box vs content-box)background-clipanswers: “Where is the background allowed to paint?” (important for rounded corners and borders)
A practical example: when I use background-attachment: fixed for a hero, I almost always combine it with background-size: cover and background-position: center so I don’t get weird tiling seams or an obvious “top-left” anchor.
On the other hand, when I use background-attachment: local inside scroll containers, I usually avoid background-size: cover if the background is meant to be a subtle texture, because cover can look like it “slides” under content as the container resizes. A repeating pattern with a small size is often more stable and less distracting.
The Two Scrolls Problem (and Why local Exists)
Most of the confusion around background-attachment comes from the fact that modern UIs commonly have multiple scroll surfaces.
When I’m debugging, I literally name them:
- Viewport scroll: the document scrolling in the main window.
- Component scroll: a scrollable panel, modal body, sidebar, or table wrapper.
Now, apply the attachments:
scroll: moves with the element as it moves in the viewport.fixed: stays pinned to the viewport.local: moves with the scrollable content inside the element.
The “aha” moment for many people is that scroll does not mean “scroll with the content.” It means “move with the element’s box as the page scrolls.” That’s why local is so useful for scrollable panels: it’s the only value that truly follows the element’s internal scroll.
Production Pattern: Scrollable Panel With a “Document” Feel
If you’ve ever built an app where a panel feels like “a sheet of paper” or “a document viewer,” you’ve already felt the ergonomics of local.
This pattern is a staple for:
- Activity logs
- Markdown preview panes
- Changelog drawers
- Settings panels
- Side-by-side diff viewers
The goal is subtle: as users scroll the panel, the background should be part of the content, not a stationary layer behind it. That avoids a “shimmer” effect where the content moves but the texture doesn’t.
Here’s a minimal CSS pattern I use (you can apply it to your own component styles):
.panel {
border-radius: 14px;
border: 1px solid rgba(255, 255, 255, 0.12);
background-color: rgba(255, 255, 255, 0.04);
}
.panelbody {
max-height: 60vh;
overflow: auto;
padding: 16px;
background-image: linear-gradient(180deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0));
background-attachment: local;
}
Two practical notes:
1) background-attachment: local only pays off if the element can actually scroll. That means a fixed height/max-height plus overflow.
2) If you use a sticky header inside the panel, the difference is even more noticeable (the background scrolls under the sticky header, as if the header is sitting on top of a page).
Production Pattern: Hero “Parallax” Without a Library
When I want the “content slides over a stationary background” vibe, I reach for background-attachment: fixed first—but only if the layout still looks good when that effect is unavailable.
That second requirement is key, because on many mobile devices fixed is either ignored, heavily constrained, or behaves differently depending on nested containers.
My simple rule:
- If the page still looks great with
background-attachment: scroll, thenfixedis a nice enhancement. - If the page looks broken without
fixed, then I don’t ship it asbackground-attachment: fixed. I use a different approach (more on that later).
In 2026, I also treat prefers-reduced-motion as a first-class switch. It’s not only about motion sickness; it’s also about respecting user preference for fewer visual effects.
Mobile and Safari Reality Check (and How I Design Around It)
If you’ve ever set background-attachment: fixed, tested on desktop, and then had a teammate say “It doesn’t work on my phone,” you’re not alone.
The most important mindset shift is this: treat background-attachment: fixed as an enhancement, not a guarantee.
What I do in practice:
- I choose backgrounds that still look cohesive when they scroll with content.
- I avoid layouts where fixed attachment is needed for contrast.
- I use overlays (gradient layers) to guarantee text readability in both modes.
If you need the effect reliably on mobile, I typically switch strategies:
- Put the “mural” on a dedicated element (often a pseudo-element) with
position: fixed. - Keep the content in a normal flow layer above it.
- Use
prefers-reduced-motionto disable transforms/animations if you add them.
Here’s a simple, production-friendly pattern that behaves consistently because it doesn’t rely on background-attachment: fixed at all:
body {
margin: 0;
background: #080b14;
color: rgba(255, 255, 255, 0.92);
}
.bg-mural {
position: fixed;
inset: 0;
z-index: -1;
pointer-events: none;
background:
radial-gradient(900px 520px at 30% 20%, rgba(0, 200, 255, 0.25), transparent 60%),
radial-gradient(700px 420px at 80% 20%, rgba(255, 120, 200, 0.18), transparent 55%),
linear-gradient(180deg, rgba(25, 40, 85, 0.85), rgba(0, 0, 0, 0.80));
}
.page {
position: relative;
z-index: 0;
}
I like this because it’s explicit: there’s a fixed layer, and your content scrolls above it. No surprise interaction with complex background painting rules.
Nested Scroll Containers: The “Why Is Fixed Not Fixed?” Bug
One of the trickiest bugs I see is: “I set background-attachment: fixed but it behaves like scroll.”
Common culprits in real apps:
- A parent element uses
transform(eventransform: translateZ(0)ortransform: translate3d(...)) to enable GPU compositing. - A parent uses
filter,backdrop-filter, orperspective. - The element is inside a scroller that isn’t the main page scroll (common in SPAs, especially when the body is locked and a wrapper handles scroll).
Different browsers handle these cases differently, but the practical takeaway is consistent: some properties create new containing contexts and can change how “fixed” behaves.
My debugging approach is boring but effective:
1) Temporarily remove transforms and filters from ancestor layers.
2) Test background-attachment: fixed again.
3) If it starts working, decide whether you can move that transform/filter to another layer, or switch to the explicit fixed-element pattern above.
If you’re using a UI framework that applies transforms to an app root for animation, the fixed-element approach is usually the most robust.
Performance: Where background-attachment Can Hurt You
On paper, background-attachment is just a painting choice. In practice, it can affect how often the browser has to repaint pixels during scroll.
Here’s my pragmatic performance checklist:
- Limit the number of
fixedlayers. One fixed background on a page is often fine; stacking several fixed backgrounds across multiple sections can get expensive. - Prefer gradients over huge image files when the goal is “texture” or “atmosphere.” Gradients are often cheaper and scale cleanly.
- If you do use images, compress aggressively and size them appropriately for the largest viewport you expect.
- Avoid giant backgrounds on small devices where memory bandwidth is tight.
- Watch for “jank” on low-end hardware. If you only test on a fast laptop, you’ll miss the real-world feel.
I avoid claiming exact numbers because performance depends on the browser and the device, but I do see repeatable patterns:
- Smooth 60fps scroll on a modern desktop can turn into “micro-stutter” on older phones when
background-attachment: fixedforces more frequent repaints. - Transparent overlays plus blur effects (
backdrop-filter) can amplify this, because you’re asking the browser to blend many layers.
If you want to verify, use your browser’s performance tools and record a scroll. Look for heavy “Paint” time or frequent rasterization work.
Accessibility: Motion, Contrast, and Readability
I don’t treat background-attachment as an “animation,” but users often experience fixed as motion because the foreground content moves while the background stays still.
What I do in production:
- Always include a
prefers-reduced-motionfallback forfixedeffects. - Maintain text contrast with overlay layers rather than relying on the background image itself.
- Avoid high-frequency textures behind body text (thin repeating lines can create a visual vibration while scrolling).
A quick pattern I reuse:
.hero {
background-attachment: fixed;
}
@media (prefers-reduced-motion: reduce) {
.hero {
background-attachment: scroll;
}
}
This doesn’t “ruin” the design; it simply respects users who asked for fewer effects.
Common Pitfalls (and the Fixes I Reach For)
Here are the mistakes I see most often, plus what I do about them.
1) “local doesn’t do anything.”
– Fix: ensure the element actually scrolls (max-height + overflow: auto) and has enough content to scroll.
2) “fixed works on desktop but not on mobile.”
– Fix: treat fixed as an enhancement; design for scroll as baseline; use an explicit fixed element if you need reliability.
3) “The background jumps or jitters.”
– Fix: simplify layers; avoid large image files; reduce repaints by limiting fixed usage; check for expensive effects like blur.
4) “The background ignores rounded corners.”
– Fix: confirm background-clip (often padding-box helps) and ensure the element has border-radius. If you’re doing complex clipping, a pseudo-element with border-radius: inherit can be cleaner.
5) “Nothing is fixed inside my app shell.”
– Fix: check whether your app root has transforms/filters or whether you’re scrolling inside a wrapper element. Consider moving the fixed layer outside that wrapper.
When to Use It vs When to Avoid It (My 2026 Rules of Thumb)
I like having a decision rubric so I don’t argue with myself on every page.
I reach for background-attachment: scroll when:
- The background is decorative and should behave like part of the component.
- I want predictable behavior across browsers with minimal surprises.
- The component might be reused inside scroll containers.
I reach for background-attachment: local when:
- The element itself scrolls and I want the background to feel like it’s part of the content.
- I’m building product UI panels (not full-page marketing sections).
- I’m using subtle textures that would look “stuck” if they didn’t move with content.
I consider background-attachment: fixed when:
- The page is a classic document scroll (not a nested scroller).
- The effect is purely decorative and the layout still looks good when it degrades to
scroll. - I can afford the performance cost and I’ve tested on lower-end devices.
I avoid background-attachment: fixed when:
- The design relies on it for legibility.
- The page uses complex nested scrolling (app shells, modals, drawers).
- The brand background is a high-resolution photo that would require heavy repainting.
Alternatives to background-attachment: fixed (When You Need More Control)
Sometimes background-attachment: fixed is the right idea but the wrong tool.
Here are the alternatives I reach for, in order:
1) Explicit fixed layer (recommended for reliability)
– Use a position: fixed element behind your content (like .bg-mural earlier).
2) Pseudo-element background on a wrapper
– Useful when you want the background bound to a specific section instead of the whole page.
3) Transform-based parallax
– Only when you truly need “different scroll speeds,” not just a pinned background.
– Use carefully; respect prefers-reduced-motion.
4) Static art direction
– Sometimes the simplest solution is to remove the effect and improve composition/typography.
– If the content is great, you don’t need the background to do acrobatics.
Debug Checklist: What I Check in 60 Seconds
When a teammate pings me “background attachment is broken,” I run this checklist.
- Am I scrolling the page or an element? (If it’s an element,
localmight be the intent.) - Does the element actually scroll? (
overflow+ constrained height) - Are there multiple background layers with mismatched lists? (Count commas.)
- Is a parent applying transforms/filters that can change fixed behavior?
- Is this mobile Safari or another mobile browser with constraints?
- Is there a reduced-motion mode or a CSS reset overriding my declaration?
If I can’t resolve it quickly, I create a minimal reproduction by stripping the layout down to one element with a background and one scroll axis. That almost always reveals whether the issue is conceptual (wrong attachment) or contextual (layout/containment).
Practical Tips for Shipping Backgrounds Safely
A few habits have saved me from late-night CSS surprises:
- Always provide a readable overlay when using photos or bright gradients. Don’t depend on the image staying “just so” on every viewport.
- Design for the baseline (
scroll) first, then addfixedas a progressive enhancement. - Avoid using fixed effects inside scrolling app shells unless you control the entire scroll pipeline.
- Test with real content length, because attachment effects are invisible on short pages.
- Keep the background subtle behind long-form text. People are reading; don’t make their eyes work harder.
Quick Reference (Mental Model in One Screen)
If you only remember one thing, remember this:
scroll: background moves with the element as the page scrolls.fixed: background stays pinned to the viewport.local: background scrolls with the element’s internal scrollable content.
And if you remember two things:
fixedis often an enhancement, not a promise.localis the secret weapon for scrollable panels.


