greensock/GSAP · error

ScrollSmoother needs a valid content element.

Error message

ScrollSmoother needs a valid content element.

What it means

ScrollSmoother must know which element wraps the scrolling content (default selector "#smooth-content"). The content setter warns "ScrollSmoother needs a valid content element." when _toArray resolves nothing, then falls back to _body.children[0].

Source

Thrown at src/ScrollSmoother.js:377

				startupPhase ? ScrollTrigger.refresh() : adjustParallaxPosition([st], true); // all the effects need to go through the initial full refresh() so that all the pins and ratios and offsets are set up. That's why we do a full refresh() if it's during the startupPhase.
			}
			y = st.start / (ignoreSpeed ? speed : 1);
			st.kill(false);
			target.style.cssText = cssText;
			gsap.core.getCache(target).uncache = 1;
			return y;
		};

		function refreshHeight() {
			height = content.clientHeight;
			content.style.overflow = "visible"
			_body.style.height = (_win.innerHeight + (height - _win.innerHeight) / speed) + "px";
			return (height - _win.innerHeight);
		}

		this.content = function(element) {
			if (arguments.length) {
				let newContent = _toArray(element || "#smooth-content")[0] || console.warn("ScrollSmoother needs a valid content element.") || _body.children[0];
				if (newContent !== content) {
					content = newContent;
					contentCSS = content.getAttribute("style") || "";
					resizeObserver && resizeObserver.observe(content);
					gsap.set(content, {overflow: "visible", width: "100%", boxSizing: "border-box", y: "+=0"});
					smoothDuration || gsap.set(content, {clearProps: "transform"});
				}
				return this;
			}
			return content;
		}

		this.wrapper = function(element) {
			if (arguments.length) {
				wrapper = _toArray(element || "#smooth-wrapper")[0] || _wrap(content);
				wrapperCSS = wrapper.getAttribute("style") || "";
				refreshHeight();
				gsap.set(wrapper, smoothDuration ? {overflow: "hidden", position: "fixed", height: "100%", width: "100%", top: 0, left: 0, right: 0, bottom: 0} : {overflow: "visible", position: "relative", width: "100%", height: "auto", top: "auto", bottom: "auto", left: "auto", right: "auto"});

View on GitHub (pinned to 13e2b79054)

Solutions

  1. Add a wrapper element with id="smooth-content" (or set vars.content to your existing wrapper selector)
  2. Ensure the content element exists in the DOM before creating the smoother
  3. Pass an actual Element reference as content instead of a selector
  4. Create the smoother inside a useEffect/useMounted hook after layout renders

Example fix

// before
const smoother = ScrollSmoother.create({ smooth: 1 }); // no #smooth-content in DOM
// after
<div id="smooth-wrapper"><div id="smooth-content">...</div></div>
const smoother = ScrollSmoother.create({ smooth: 1, content: '#smooth-content' });
Defensive patterns

Strategy: validation

Validate before calling

const contentSel = vars?.content || '#smooth-content';
if (!document.querySelector(contentSel)) {
  throw new Error(`ScrollSmoother content element missing: ${contentSel}`);
}
const smoother = ScrollSmoother.create({ content: contentSel, ...vars });

Type guard

function isElement(v) { return v instanceof Element; }

Prevention

When it happens

Trigger: Creating a smoother when no element matches vars.content (default "#smooth-content") and the page body has no children to fall back on, or passing content: "#wrong-id"; also calling smoother.content(el) with a selector that matches nothing.

Common situations: Forgot to add the #smooth-content wrapper div in markup, content element rendered after smoother creation in an SPA, typo in the content selector, or the plugin instantiated before React/Vue mounts the layout.

Related errors


AI-assisted analysis of greensock/GSAP@13e2b79054 (2026-08-29). Data as JSON: /api/errors/1b471bc2ad45e414. Report an issue: GitHub.