A few years ago, I debugged a landing page that felt “jittery” on scroll. The hero image looked like it was sliding at a different speed than the text, and on some phones the background stuttered so badly the CTA became unreadable. The root cause wasn’t JavaScript—it was a single CSS line that controlled how the background image moved relative to the viewport and the element. If you build interfaces where scroll tells the story, or you just want a stable visual anchor behind content, you should be deliberate about background-attachment. In this guide, I’ll walk you through exactly how the property behaves, where it shines, where it bites, and how to design for it in real UI. You’ll get clean mental models, complete runnable snippets, and the patterns I use in production when I need reliable background behavior across devices and complex layouts.
The Mental Model I Use: Three “Frames of Reference”
When I decide between scroll, fixed, and local, I picture three different coordinate systems:
- Element space: the background is tied to the element’s box.
- Viewport space: the background is tied to the screen, not the content.
- Content space: the background moves with the element’s scrollable content.
That’s it. Everything else—parallax effects, sticky hero banners, scrollable cards—falls out of those frames. The property doesn’t change what the image is; it changes what the image is attached to.
Here’s the canonical syntax:
background-attachment: scroll fixed local initial inherit;
And here’s the behavioral summary I carry in my head:
scroll: background moves with the page as the element moves. This is the default.fixed: background stays pinned to the viewport.local: background moves with the element’s scrollable content.
If you keep that frame-of-reference model in mind, you’ll avoid 90% of the confusion that I see in code reviews.
Default Behavior: scroll and the “Cardboard Box” Analogy
I explain scroll like this: imagine the element is a cardboard box with an image printed on the inside. When you move the box, the image moves with it. That’s scroll. The background is tied to the element’s box, not the viewport.
Here’s a complete runnable example that uses scroll. The background will move as the element moves in the page:
background-attachment: scroll
body {
font-family: "Source Serif 4", serif;
margin: 0;
background: #f7f3ee;
color: #2a2a2a;
}
.section {
min-height: 120vh;
padding: 40px 24px;
display: flex;
align-items: center;
justify-content: center;
}
.panel {
width: min(900px, 92vw);
padding: 48px;
background-image: url("https://images.unsplash.com/photo-1498050108023-c5249f4df085?q=80&w=1600&auto=format&fit=crop");
background-position: center;
background-repeat: no-repeat;
background-size: cover;
background-attachment: scroll;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
}
.panel h1 {
margin: 0 0 12px;
font-size: clamp(28px, 4vw, 44px);
}
.panel p {
margin: 0;
font-size: 18px;
line-height: 1.6;
background: rgba(255, 255, 255, 0.75);
padding: 16px 20px;
border-radius: 12px;
}
Scroll-attached background
As you scroll, this background moves with the element. Think of it as
printed inside the element’s box.
In practice, scroll is the safest choice for performance and cross-device behavior. If you’re building a component library or a design system, make scroll your baseline unless there’s a specific reason to override it.
Pinned to the Screen: fixed as a Visual Anchor
fixed does exactly what it says: it pins the background to the viewport. The element can move, but the background doesn’t. I compare it to a wall poster behind a moving picture frame. The frame (element) moves; the poster (background) stays put.
This is the most common way to simulate a simple parallax effect without JavaScript. It looks great on desktops and large displays, but it can be tricky on mobile (we’ll get to that).
Here’s a full demo:
background-attachment: fixed
:root {
--paper: #f0efe8;
--ink: #2a2a2a;
}
body {
margin: 0;
font-family: "IBM Plex Sans", sans-serif;
background: var(--paper);
color: var(--ink);
}
.hero {
min-height: 140vh;
padding: 64px 24px;
background-image: url("https://images.unsplash.com/photo-1517694712202-14dd9538aa97?q=80&w=1800&auto=format&fit=crop");
background-size: cover;
background-position: center;
background-repeat: no-repeat;
background-attachment: fixed;
display: flex;
align-items: center;
justify-content: center;
}
.hero .card {
width: min(720px, 92vw);
background: rgba(255, 255, 255, 0.8);
padding: 32px 36px;
border-radius: 14px;
backdrop-filter: blur(6px);
}
.hero h2 {
margin: 0 0 12px;
font-size: clamp(26px, 4vw, 40px);
}
.hero p {
margin: 0;
line-height: 1.7;
font-size: 18px;
}
Fixed background effect
The image is pinned to the viewport. As the content scrolls, you see a
window over a static scene.
When I want a calm, stable backdrop for a feature section, this is my go‑to. If the background is detailed, I’ll add an overlay or blur to keep the text readable. You should also consider performance on low-powered devices, because the browser may need to repaint the fixed image on every scroll tick.
Scrolling Inside the Element: local for Scrollable Containers
local is the least used value, but it’s my favorite for certain UI patterns. It means the background moves with the scrollable content inside an element. So if you have a card with overflow: auto, the background scrolls as the content scrolls.
I think of it as the background being painted on the scrollable content layer, not on the element’s border box. This matters in nested layouts like chat panes, code viewers, and dashboards where scroll happens inside a container rather than the page.
Here’s a runnable example that shows local clearly:
background-attachment: local
body {
margin: 0;
font-family: "Work Sans", sans-serif;
background: #f6f2ed;
color: #2a2a2a;
display: grid;
place-items: center;
min-height: 100vh;
}
.log-panel {
width: min(720px, 92vw);
height: 360px;
padding: 20px;
overflow: auto;
border-radius: 14px;
background-image: url("https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?q=80&w=1600&auto=format&fit=crop");
background-size: cover;
background-repeat: no-repeat;
background-position: center;
background-attachment: local;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.15);
}
.log-panel p {
margin: 0 0 18px;
padding: 12px 14px;
background: rgba(255, 255, 255, 0.78);
border-radius: 10px;
line-height: 1.6;
}
09:04:12 — Deployment started. Building bundles...
09:04:19 — Bundle sizes within threshold. Uploading assets...
09:04:31 — Edge cache warmed. Health checks running...
09:04:44 — Latency stable. Canary live at 10% traffic.
09:04:58 — Error rate below 0.1%. Scaling up to 100%.
09:05:10 — Deployment complete. Monitoring after-action metrics.
09:05:24 — Post-release checks healthy. No regression signals.
09:05:36 — Incident log archived. Closing release ticket.
09:05:50 — End of log. Scroll to review the earlier entries.
If you ever felt like a background inside a scrollable card looked “stuck,” that’s because you probably left it at scroll. Switching to local makes the background move with the content, which feels more natural in that context.
Practical Use Cases I Reach For in Production
Here are the patterns where I intentionally choose a specific attachment value:
- Editorial hero with a calm backdrop:
fixedfor large, visual headers where the content scrolls over a stable scene. - Reusable content cards:
scrollso the background behaves predictably in any layout. - Scrollable panels and sidebars:
localwhen the scrolling happens inside the container. - Timeline sections:
fixedif I want a slow, cinematic feel (but only on desktop). - Performance-sensitive pages:
scrollas the default to avoid repaint overhead.
I always ask: “What is the user scrolling?” If the user scrolls the page, fixed can be expressive. If the user scrolls the container, local is more intuitive. If you want no surprises, stick with scroll.
Common Mistakes I See (and How I Avoid Them)
I’ve reviewed a lot of CSS where background-attachment caused subtle bugs. Here are the ones I still watch for in 2026:
- Using
fixedon mobile without a fallback: Some mobile browsers treatfixedasscrollor repaint it in a way that creates visible lag. I always add a media query fallback. - Forgetting background size: Without
background-size: coverorcontain, the effect can look like a repeating stamp instead of a hero image. - No contrast management: Fixed backgrounds can make text unreadable. I typically add a semi-transparent overlay or a blur layer.
- Misunderstanding
local: It only shows its behavior when the element is actually scrollable (overflow: autoorscroll). If the element doesn’t scroll, it looks identical toscroll. - Combining with
background-clipand expecting magic: Attachment affects painting context, not clipping. If you need a clipped effect, definebackground-clipexplicitly.
A short example of a mobile-safe fallback for fixed:
.hero {
background-attachment: fixed;
}
@media (max-width: 820px), (pointer: coarse) {
.hero {
background-attachment: scroll; / smoother on mobile /
}
}
I tend to combine pointer: coarse with a width check because screen size alone isn’t enough in a world of tablets and foldables.
Performance and Rendering Considerations (Realistic Ranges)
I care about scroll performance, especially on long pages. The attachment value can shift work between CPU and GPU. Here’s how I see it in practice:
scrollis usually the cheapest. The background is part of the element and moves with it.fixedcan add repaint work on scroll, often adding a few milliseconds per frame on mid‑range devices. In busy layouts, I’ve seen it cost roughly 8–15ms during fast scroll, which can stutter at 60fps.localsits in the middle. It’s fine for small scrollable areas, but heavy content plus a large image can still add a few milliseconds during scroll.
If you want a parallax effect but also need steady performance, I recommend either:
- Using
scrollplus a subtle gradient overlay to fake depth, or - Building a lightweight transform-based parallax (with
will-change: transform) and throttling updates torequestAnimationFrame.
But for most content pages, scroll or fixed is enough, and the simplest choice is usually the right one.
Background Attachment in Multi-Layered Backgrounds
A detail that often surprises developers is that background-attachment can apply to multiple background layers. If you provide multiple images, you can attach each one differently. That allows for layered effects without scripting.
Example: one background is fixed (a subtle texture), the other scrolls with content:
Multiple background layers
body {
margin: 0;
font-family: "Manrope", sans-serif;
background: #f4f1ea;
}
.multi-layer {
min-height: 140vh;
padding: 64px 24px;
color: #1f1f1f;
background-image:
url("https://images.unsplash.com/photo-1441974231531-c6227db76b6e?q=80&w=1600&auto=format&fit=crop"),
linear-gradient(120deg, rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0.85));
background-repeat: no-repeat, no-repeat;
background-size: cover, cover;
background-position: center, center;
background-attachment: fixed, scroll;
}
.multi-layer h2 {
margin: 0 0 12px;
font-size: clamp(26px, 4vw, 40px);
}
.multi-layer p {
max-width: 720px;
line-height: 1.7;
font-size: 18px;
}
Layered attachment
The photo layer is fixed, while the gradient layer scrolls with the content.
This keeps text legible without sacrificing the depth effect.
This technique is a reliable way to create depth while keeping text contrast stable. I use it for long-form articles and product feature pages where a single image might be too busy.
When to Use fixed vs. a Transform-Based Parallax
background-attachment: fixed is the simplest parallax, but it’s not always the best. Here’s how I decide:
I choose
—
fixed
scroll + overlay
transform-based JS
local
I don’t default to JavaScript unless I need multi-layer motion or timing control. CSS background-attachment gives you a stable, declarative option with far less code.
Edge Cases and Layout Interactions You Should Know
A few less obvious interactions can trip you up:
background-attachment: fixedinside transformed elements: When any ancestor hastransform,filter, orperspective, many browsers treat the fixed background as if it were attached to that ancestor instead of the viewport. The result looks like a broken parallax. My fix is to avoid transforms on parents of fixed-background sections or move the fixed background to a top-level wrapper that isn’t transformed.- Nested scroll containers: If the element is inside a scrollable parent,
fixedstill ties to the viewport, not the parent. That can feel visually “wrong” in modals or sheets. In those cases, I either switch toscrollor use a pseudo-element andposition: stickyfor a controlled effect. localwithout enough overflow: If your container’s content doesn’t overflow,localis indistinguishable fromscroll. I check withscrollbar-gutter: stableor a simplemin-heightstress test.- Composited layers and
background-clip: If you’re clipping backgrounds to padding or text, the paint order can feel surprising withfixed. I keepbackground-clip: padding-boxexplicit to avoid odd edges. - Printing and PDF export: Some print engines drop fixed backgrounds. If you need reliable output in PDFs, add a print stylesheet that switches
fixedtoscrolland increases contrast.
These are edge cases, but they show up in real projects—especially in apps with complex layout stacks, transforms, and modal portals.
A Deeper Mental Model: Painting Areas, Boxes, and Scroll Containers
The three frames of reference are a great starting point, but you’ll get more predictable results if you also think about where a background is painted.
There are three related properties that shape the final result:
background-origin: which box the image is positioned against (padding-box,border-box, orcontent-box).background-clip: which box it’s clipped to.background-attachment: which scrolling context it’s attached to.
I visualize it like this:
- Pick the stage (
background-attachment): element, viewport, or scrollable content. - Pick the anchor (
background-origin): the corner of the box you position from. - Pick the mask (
background-clip): the box you clip to so it doesn’t bleed.
If you set all three intentionally, most “mysterious” bugs disappear. Here’s a small pattern I use when I need a fixed background that doesn’t bleed under a border:
.hero {
border: 8px solid #fff;
background-image: url("/images/hero.jpg");
background-attachment: fixed;
background-origin: padding-box;
background-clip: padding-box;
background-position: center;
background-size: cover;
}
The origin and clip match, so the image is positioned and clipped within the same box. That keeps the border crisp and prevents the “glow” you can get when the image is drawn under semi‑transparent borders.
Fixed Backgrounds Without the Jank (A Reliable Pattern)
I like background-attachment: fixed, but I rarely use it on the element itself when I’m building a production UI. A more reliable pattern is to simulate it with a pseudo-element or a separate layer. That gives me better control over stacking, opacity, and fallbacks.
Here’s the pattern I use most often:
<div class="heroinner">
Launch your idea
Ship faster with a calm, stable background that keeps focus on the copy.


