CSS background-attachment: A Practical Deep Dive

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: fixed for large, visual headers where the content scrolls over a stable scene.
  • Reusable content cards: scroll so the background behaves predictably in any layout.
  • Scrollable panels and sidebars: local when the scrolling happens inside the container.
  • Timeline sections: fixed if I want a slow, cinematic feel (but only on desktop).
  • Performance-sensitive pages: scroll as 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 fixed on mobile without a fallback: Some mobile browsers treat fixed as scroll or repaint it in a way that creates visible lag. I always add a media query fallback.
  • Forgetting background size: Without background-size: cover or contain, 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: auto or scroll). If the element doesn’t scroll, it looks identical to scroll.
  • Combining with background-clip and expecting magic: Attachment affects painting context, not clipping. If you need a clipped effect, define background-clip explicitly.

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:

  • scroll is usually the cheapest. The background is part of the element and moves with it.
  • fixed can 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.
  • local sits 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 scroll plus a subtle gradient overlay to fake depth, or
  • Building a lightweight transform-based parallax (with will-change: transform) and throttling updates to requestAnimationFrame.

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:

Scenario

I choose

Why —

— Hero section on desktop-only landing page

fixed

Simple, minimal code, good effect Mobile-first design with heavy imagery

scroll + overlay

Smoother scrolling and better battery Complex parallax across multiple sections

transform-based JS

Fine-grained control and predictable performance Scrollable card inside a dashboard

local

The user scrolls the container, not the page

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: fixed inside transformed elements: When any ancestor has transform, filter, or perspective, 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, fixed still ties to the viewport, not the parent. That can feel visually “wrong” in modals or sheets. In those cases, I either switch to scroll or use a pseudo-element and position: sticky for a controlled effect.
  • local without enough overflow: If your container’s content doesn’t overflow, local is indistinguishable from scroll. I check with scrollbar-gutter: stable or a simple min-height stress test.
  • Composited layers and background-clip: If you’re clipping backgrounds to padding or text, the paint order can feel surprising with fixed. I keep background-clip: padding-box explicit 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 fixed to scroll and 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, or content-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.

.hero {

position: relative;

min-height: 120vh;

overflow: hidden;

color: #1f1f1f;

}

.hero::before {

content: "";

position: fixed;

inset: 0;

z-index: -1;

background-image: url("/images/hero.jpg");

background-size: cover;

background-position: center;

transform: translateZ(0);

}

.heroinner {

max-width: 720px;

padding: 72px 24px;

margin: 0 auto;

background: rgba(255, 255, 255, 0.8);

border-radius: 18px;

}

@media (max-width: 820px), (pointer: coarse) {

.hero::before {

position: absolute;

}

}

This approach avoids the browser quirks around background-attachment: fixed because I’m not relying on the attachment property at all. I’m still getting a fixed effect, but I have explicit control over the layer. When I need a guaranteed experience across browsers, I pick this pattern.

Practical Recipes: Real UI Patterns You Can Reuse

I keep a small library of background-attachment “recipes” so I can drop them into projects without rethinking the whole layout. These are the ones I use most often.

Recipe 1: Editorial Hero With Subtle Depth

The goal: a calm, cinematic hero on desktop with a safe fallback on mobile.

.hero {

min-height: 130vh;

padding: 72px 24px;

color: #fff;

background-image:

linear-gradient(180deg, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.25)),

url("/images/mountains.jpg");

background-size: cover;

background-position: center;

background-repeat: no-repeat;

background-attachment: fixed, fixed;

}

.hero h1 {

font-size: clamp(36px, 5vw, 64px);

max-width: 22ch;

}

@media (max-width: 900px), (pointer: coarse) {

.hero {

background-attachment: scroll, scroll;

}

}

This gives you the cinematic feel on large screens while keeping mobile smooth.

Recipe 2: Scrollable Card With a “Living” Background

The goal: a scrollable content panel where the background moves with the content.

.card {

width: min(720px, 92vw);

height: 420px;

overflow: auto;

padding: 24px;

border-radius: 16px;

background-image: url("/images/noise.png"), url("/images/pattern.jpg");

background-size: 220px 220px, cover;

background-repeat: repeat, no-repeat;

background-position: center, center;

background-attachment: local, local;

}

.card p {

background: rgba(255, 255, 255, 0.85);

padding: 12px 14px;

border-radius: 10px;

}

Here, both layers move with the internal scroll so the background feels anchored to the content.

Recipe 3: A Section Divider That Feels Like a Window

The goal: a short section between blocks of content that looks like you’re peeking into a fixed scene.

.window {

min-height: 60vh;

background-image: url("/images/city.jpg");

background-attachment: fixed;

background-size: cover;

background-position: center;

}

.windowoverlay {

min-height: 60vh;

display: grid;

place-items: center;

background: rgba(0, 0, 0, 0.35);

color: #fff;

}

That “window” effect is powerful for breaking long articles into visual beats.

Recipe 4: Sticky Sidebar Illusion (Without JavaScript)

The goal: a sidebar that feels visually anchored with a subtle fixed texture.

.layout {

display: grid;

grid-template-columns: minmax(0, 1fr) 280px;

gap: 32px;

}

.sidebar {

position: sticky;

top: 32px;

align-self: start;

background-image: url("/images/linen.png");

background-attachment: fixed;

background-repeat: repeat;

background-size: 200px 200px;

padding: 24px;

border-radius: 12px;

}

This works best on desktop. For smaller screens, I switch to a normal scroll background so the sidebar doesn’t feel detached.

Accessibility and Readability: Contrast Rules That Actually Work

Most background-attachment problems show up as readability problems. If the background moves unpredictably, the text becomes hard to track. I use a few rules of thumb:

  • Always keep body text above 4.5:1 contrast. If the image is dynamic, add a gradient or a semi‑transparent overlay.
  • Use text containers, not just text shadows. Shadows help, but they aren’t a substitute for a stable surface behind text.
  • Reduce visual noise under small text. Fine patterns (like noisy textures) are okay under large headings, but they make paragraph text hard to scan.
  • Respect motion preferences. If a user has prefers-reduced-motion: reduce, I eliminate fixed backgrounds and parallax effects.

Here’s a lightweight overlay pattern I use all the time:

.section {

background-image: url("/images/forest.jpg");

background-attachment: fixed;

background-size: cover;

background-position: center;

position: relative;

}

.section::after {

content: "";

position: absolute;

inset: 0;

background: linear-gradient(120deg, rgba(0, 0, 0, 0.55), rgba(0, 0, 0, 0.2));

}

.section > * {

position: relative;

z-index: 1;

}

With this pattern, the background can move, but the text stays readable because the overlay stabilizes contrast.

Responsive Strategy: Treat Attachment as Progressive Enhancement

I don’t treat background-attachment: fixed as a baseline. I treat it as a progressive enhancement that I enable on devices that can handle it.

Here’s how I think about it:

  • Baseline: scroll everywhere.
  • Enhancement: fixed on larger screens with precise pointer input.
  • Fallback: scroll on small screens or coarse pointers.

Here’s the exact pattern I ship most often:

.hero {

background-attachment: scroll;

}

@media (min-width: 960px) and (pointer: fine) {

.hero {

background-attachment: fixed;

}

}

@media (prefers-reduced-motion: reduce) {

.hero {

background-attachment: scroll;

}

}

That last media query matters. The fixed effect is a form of motion. It’s subtle, but it is still motion. I opt out of it for users who explicitly want reduced motion.

Debugging Checklist: The 60-Second Triage

When a background attachment looks wrong, I use this quick checklist before diving into anything complex:

  • Is the element actually scrollable? If not, local won’t show up.
  • Does an ancestor have transform, filter, or perspective? If yes, fixed may behave like scroll.
  • Is there a background-size set? Missing size often causes weird tiling.
  • Is there a clipped or padded box? Check background-origin and background-clip.
  • Is the device coarse pointer or low-end? If yes, switch to scroll and confirm performance.
  • Are multiple backgrounds applied? Make sure the attachment list matches the layer count.

Most bugs show up in the first two checks. I’ve saved hours by inspecting the DOM for a sneaky transform on a parent.

Alternatives and Complements: When I Don’t Use background-attachment

There are times when background-attachment isn’t the right tool. Here’s what I do instead.

Option 1: Use a fixed-position layer

This is my go-to when browser support is weird or I need precise stacking control. You saw this earlier with the pseudo-element trick. It’s explicit and predictable.

Option 2: Use background-attachment: scroll and fake depth

If I want depth without the repaint cost, I use a gradient overlay, subtle shadows, or a mild blur on the text container. It reads as layered even though the background moves normally.

Option 3: Use a transform-based parallax

If I need to animate layers at different speeds, I use transform: translateY(...) on absolutely positioned layers and drive it with requestAnimationFrame. This is more code, but I can throttle, pause, and fine-tune it.

A tiny, safe parallax pattern looks like this:

<div class="parallaxbg" aria-hidden="true">
<div class="parallaxcontent">

Focus on the story

The background moves gently, but it never steals attention.

.parallax {

position: relative;

min-height: 100vh;

overflow: hidden;

}

.parallaxbg {

position: absolute;

inset: -10% 0;

background-image: url("/images/ocean.jpg");

background-size: cover;

background-position: center;

will-change: transform;

}

.parallaxcontent {

position: relative;

z-index: 1;

padding: 72px 24px;

}

document.addEventListener("scroll", () => {

const bg = document.querySelector(".parallaxbg");

const offset = window.scrollY * 0.2;

bg.style.transform = translateY(${offset}px);

});

If you use this, add a requestAnimationFrame wrapper and a prefers-reduced-motion check in production.

Background Attachment in Component Libraries and Design Systems

If you maintain a design system, background-attachment deserves a policy. I bake it into component defaults so teams don’t reinvent the same decisions.

My rules of thumb:

  • Default every component to scroll. This eliminates surprises when components are embedded in different layouts.
  • Expose attachment as a variant for hero-like sections. When a designer asks for a cinematic effect, they opt into it consciously.
  • Add a mobile-safe token. I expose something like --attachment-hero: scroll by default and switch it to fixed in desktop media queries.
  • Document the “no transforms on parents” rule. I include it in our component docs so engineers don’t break the effect later.

Here’s a small design-system pattern I’ve shipped in multiple codebases:

:root {

--hero-attachment: scroll;

}

@media (min-width: 960px) and (pointer: fine) {

:root {

--hero-attachment: fixed;

}

}

.hero {

background-attachment: var(--hero-attachment);

}

That single token keeps the rule centralized and obvious.

Testing and Performance Validation in Real Projects

I don’t ship a background attachment effect without testing it in at least three contexts:

  • A fast desktop browser (to confirm the intended visual effect).
  • A mid-range mobile device (to confirm smooth scrolling and battery usage).
  • A nested scroll container (to confirm local behaviors in cards or panels).

I also do a quick performance sanity check:

  • Chrome Performance panel: Scroll and look for long “recalculate style” or “paint” bars.
  • Safari Tech Preview: It’s a great canary for fixed background quirks.
  • DevTools layers view: If the background is re‑rasterized on every scroll, it’s a red flag.

I don’t chase perfection, but I need to know if the effect will drop frames on the devices my users actually have.

FAQ: Quick Answers to the Questions I Get Most

Does background-attachment: fixed work in all browsers?

It works in modern desktop browsers, but some mobile browsers either disable it or render it with compromises. I treat it as progressive enhancement.

Why doesn’t local look different?

Because the element isn’t scrollable. Add more content or set a fixed height with overflow: auto.

Can I use background-attachment with gradients?

Yes. Gradients are just another background layer, so they can be attached like any image.

Is background-attachment GPU accelerated?

Sometimes. It depends on the browser and the rest of the layout. That’s why I test with DevTools rather than assuming.

Should I use fixed for a parallax effect?

If you want a simple, low-code effect and you’re desktop-focused, yes. If you need precision or mobile-first performance, use a transform-based approach or a fixed-position layer.

A Simple Decision Framework I Use in Reviews

When I’m reviewing CSS in a PR, I apply this framework:

  • What is the scroll context? Page, container, or both?
  • What is the target device mix? Desktop-heavy or mobile-heavy?
  • Is the background decorative or functional? Decorative backgrounds can be heavier; functional ones must be readable.
  • Is the effect worth the cost? If it doesn’t support the content, I default to scroll.

If I can answer those in under a minute, the right attachment value becomes obvious.

Wrap-up: Make the Scroll Story Intentional

background-attachment is a deceptively small property with a big impact. When you treat it as a design decision—rather than a random CSS tweak—you get more predictable scrolling, better readability, and fewer performance headaches.

My personal default is still scroll, but I reach for fixed and local when the scroll story demands it. As long as you anchor the decision to the user’s scroll context and test on the devices that matter, you’ll get clean, reliable results.

If you want one sentence to remember: The background should be attached to the thing the user believes they’re moving. That’s the mental model that keeps the UI feeling grounded.

Scroll to Top