scroll-start: Controlling Initial Scroll Position Declaratively
A chat window or code viewer that should open mid-scroll used to mean waiting for layout, then scrolling imperatively with JavaScript. scroll-start removes that visible flash entirely.
Paul Radford — Full-Stack Developer & Editor. Paul is a full-stack developer and editor of template.tips. Articles here are AI-drafted and reviewed by Paul, who knows the code well enough to catch what's wrong and cut the hype.

The scroll-start CSS property lets you set where a scroll container's content is positioned when it first paints, without running JavaScript to call scrollTo() or scrollIntoView(). You declare an initial offset (along one or both axes) directly in CSS, and the browser applies it before the element becomes visible, avoiding the flash of an unscrolled container.
What problem does scroll-start actually solve?
Anyone who has built a chat interface, a horizontally scrolling carousel that should open mid-set, or a code viewer that needs to land on line 40 knows the usual fix: wait for layout, then imperatively scroll. That works, but it has three recurring costs.
First, there's a timing race. You need the container to exist in the DOM, have its content sized, and only then scroll it. Get the timing wrong and users see a visible jump, a flicker from top-of-content to the intended position.
Second, it couples layout intent to script execution. If JavaScript is delayed, blocked, or simply hasn't run yet (slow third-party bundle, hydration delay in a framework), the scroll position defaults to zero and then snaps later. That snap is jarring and, on constrained devices, sometimes never happens if the script errors out silently.
Third, it's an accessibility landmine. Screen reader users and keyboard users can end up with a scroll position that doesn't match what's visually happening, because the imperative scroll fires asynchronously relative to focus and rendering.
scroll-start addresses all three by moving the concern into the render tree itself. The browser knows the intended start position at layout time, so there's no race, no flash, and no dependency on script execution order.
Basic syntax and values
The property accepts one or two values, mapping to the block and inline axes (or you can use the longhand scroll-start-block and scroll-start-inline).
1/* Sets both block and inline start scroll offsets to 'top' and 'start' respectively */2.chat-log {3 scroll-start: top start;4}56/* Longhand form, useful when you only want to affect one axis */7.code-viewer {8 scroll-start-block: 400px; /* start scrolled down 400px on load */9}1011/* Percentage-based start, relative to the scrollable overflow area */12.carousel {13 scroll-start-inline: 50%;14}Accepted keyword values generally include auto (browser default, usually the zero position), start, end, top, bottom, center, plus length and percentage values. The exact keyword set and axis mapping are still being finalized in the CSSOM View / CSS Overflow specifications, so check the current working draft before relying on anything beyond auto, start, and length values in production. The CSS Overflow Module Level 5 draft is the primary spec home for this feature at the time of writing.
How is this different from scroll-snap-align or scrollIntoView?
This is the question that trips people up most, because all three sound like they do the same thing.
scroll-snap-align controls where a child snaps to during and after a scroll gesture, on every scroll interaction, not just the first one. It's a snapping behavior tied to scroll-snap-type on the ancestor, and it fires continuously as the user scrolls.
scrollIntoView() is imperative and runs whenever you call it, at any point in the element's lifetime, not just on load. It's the right tool when the "start" position depends on runtime state you can't express in CSS, such as "scroll to the message the user just sent."
scroll-start is declarative and applies exactly once: at the moment the scroll container is first laid out with overflow content. It does not re-trigger on resize, on content change, or on subsequent visits to the page within the same session (behavior here can vary; some engines reset scroll-start behavior on any fresh layout of the box, which matters if you're conditionally hiding and re-showing a container with display: none versus visibility: hidden).
A rule of thumb I use: if the position is knowable before your JS runs, use scroll-start. If it depends on data that arrives after paint (a fetched list, a websocket message, a computed match in a search result), use scrollIntoView() or manual scrollTop assignment inside a useEffect/connectedCallback, and accept that you're back in imperative territory for that specific case.
A practical pattern: chat log that opens at the bottom
1<!-- A chat container that should always open showing the latest message -->2<div class="chat-log" role="log" aria-live="polite">3 <div class="message">Hey, are you around?</div>4 <div class="message">Just saw your email, replying now.</div>5 <!-- ...many more messages... -->6 <div class="message">Sounds good, talk soon.</div>7</div>1.chat-log {2 height: 480px;3 overflow-y: auto;4 /* Declaratively start at the bottom, no JS needed on first paint */5 scroll-start-block: end;6}This removes the common "chat opens at the top, then jumps to the bottom half a second later" bug. The catch: if messages are appended after the initial paint (new message arrives via websocket), scroll-start does nothing, because it only governs the initial position. You still need your existing "stick to bottom on new message" logic for that case; scroll-start just handles the first render cleanly.
Gotchas and browser support reality check
As of early 2026, scroll-start support is not universal. It shipped behind active development in Chromium-based browsers (tracked under the CSS Overflow spec work), and cross-engine availability is inconsistent enough that you should treat it as a progressive enhancement, not a baseline requirement. Check caniuse or the MDN scroll-start reference for the current per-browser matrix before shipping it as your only mechanism for critical scroll positioning; the feature set and even property names have shifted during standardization, so older blog posts describing this property may not match the shipped syntax.
A few specific traps:
- Feature detection matters more than usual here. Because the property name and value grammar changed during the draft process, @supports (scroll-start-block: end) is safer than assuming any version tagged "scroll-start" behaves identically across browsers you test in.
- It interacts oddly with scroll-snap-type. If your container also snaps, the initial scroll-start position can be overridden by snap alignment on load in some implementations, because the browser resolves the nearest snap point after applying the start offset. Test this combination directly rather than assuming they compose cleanly.
- RTL and writing-mode content flips the inline axis. scroll-start-inline: start in a right-to-left document does not mean "left." If you support RTL layouts, verify the physical direction manually rather than assuming keyword values map the way they would in LTR.
- It does not survive history.back() scroll restoration in every browser the same way. Some browsers apply their own scroll-restoration logic on back navigation that competes with scroll-start, particularly when combined with overflow-anchor. If you rely on back-button scroll fidelity, test it explicitly rather than trusting scroll-start to own that behavior end to end.
For deeper background on how scroll offsets are computed and how this new property fits into the broader scrolling model, the MDN CSS overflow and scrolling overview is the most reliable general reference, and it links out to the more specific scroll-snap and overflow-anchor properties that often get used alongside scroll-start.
Testing this before you ship it
Don't trust a single manual check in one browser tab. Build a small test matrix:
- Load the page cold, with cache disabled, on the slowest device or throttled CPU profile you have access to in DevTools. Confirm there's no visible jump between initial paint and the intended scroll position.
- Toggle dir="rtl" on the container (or a parent) and confirm the inline-axis start position lands where you expect physically, not just logically.
- Combine the container with scroll-snap-type if your design uses both, and check whether the snap point wins on load.
- Navigate away and use the browser's back button to return, checking whether scroll restoration conflicts with your declared start position.
- Disable JavaScript entirely in DevTools and reload. If your fallback behavior (or the CSS alone) still produces an acceptable starting position, you've confirmed the enhancement degrades gracefully rather than depending on a script that might fail to load.
If your target audience includes a meaningful share of browsers that don't yet support the property, keep a minimal JavaScript fallback (a single scrollTop assignment guarded by a feature check) rather than removing it outright. The CSS property should be additive: it improves the experience where supported and costs nothing where it isn't, as long as your fallback path still runs for unsupported engines.
Related Articles

Animating to height auto using CSS interpolate-size
height: auto was never a number the browser could interpolate toward, which is exactly why it never animated at all. These two features finally give the engine something concrete to target.

aspect-ratio: The Property That Changes How You Think About Responsive Layouts
The padding-top: 56.25% trick requires position: relative on the parent and breaks badly with images that haven't loaded yet. aspect-ratio collapses all of that into a single declaration.

Controlling Scroll Boundaries with overscroll-behavior
Scrolling a chat panel to its last message and watching the whole page jump behind it is called scroll chaining, and it's almost always unwanted. One CSS property stops that handoff cold.