I'm trying to replicate something similar to the Postmates fleet website, where they use position: sticky
on a section and change elements within that section, until the user has scrolled through all the content.
Here's an example of what I mean:
So far I have set up a ref
on the section I want to make sticky:
ref={(r) => this.ref = r}
...
/>
And then I get the height of the container on page load:
componentDidMount() {
if (this.ref) {
this.setState({ targetY: this.ref.getBoundingClientRect().bottom },
// tslint:disable-next-line:no-console
() => console.log(this.state, 'state'))
}
window.addEventListener('scroll', this.handleScroll)
}
After which I detect the scrolling of the page and know when the section is in view:
handleScroll(e) {
const scrollY = window.scrollY + window.innerHeight;
const { lockedIntoView, targetY } = this.state;
// tslint:disable-next-line:no-console
console.log(scrollY, "scrollY");
if (scrollY >= targetY) {
this.setState({ lockedIntoView: true }, () => {
// tslint:disable-next-line:no-console
console.log(`LockedIntoView: ${lockedIntoView}`);
});
} else {
this.setState({ lockedIntoView: false }, () => {
// tslint:disable-next-line:no-console
console.log(`LockedIntoView: ${lockedIntoView}`);
});
}
}
Before setting the container to sticky position:
<section
...
style={{position: lockedIntoView ? "sticky" : ""}}
...
</>
I would like to know how to make it like the postmates website, where the section just remains in full view of the screen until the content has been scrolled (or for right now, until the user has scrolled a specified height)?
Or just an idea of how they've done it, and what steps I need to take in order to replicate it?
Here's my Codesandbox
Few things to consider:
1) When you set the target to position: fixed
, you are removing it from the dom flow so you need to compensate by adding some height back to the dom - example below does this on the body
.
2) You need to factor in the height of the target when checking scrollY.
3) When you pass your target height, you add the target back into the dom flow and remove the additional height added in step 1.
4) You need to keep track if we scrolling up or down - this is done but comparing the last scroll position and the current.
See comments inline below for a rough example.
styles.css:
.lockedIntoView {
position: fixed;
top: 0;
}
index.js
import * as React from "react";
import { render } from "react-dom";
import "./styles.css";
interface IProps {
title: string;
content: string;
}
interface IHeroState {
targetY: number;
lockedIntoView: boolean;
}
let lastScrollY = 0;
class App extends React.Component<IProps, IHeroState> {
ref: HTMLElement | null;
constructor(props) {
super(props);
this.handleScroll = this.handleScroll.bind(this);
this.state = {
lockedIntoView: false,
targetY: 0
};
}
componentDidMount() {
if (this.ref) {
this.setState(
{ targetY: this.ref.getBoundingClientRect().bottom },
// tslint:disable-next-line:no-console
() => console.log(this.state, "state")
);
}
window.addEventListener("scroll", this.handleScroll);
}
componentWillUnmount() {
window.removeEventListener("scroll", this.handleScroll);
}
handleScroll(e) {
const scrollY = window.scrollY + window.innerHeight;
const { lockedIntoView, targetY } = this.state;
if (lockedIntoView) {
console.info("we locked");
// update the padding on the doc as we now removing the target from normal flow
window.document.body.style.paddingBottom = `${this.ref.getBoundingClientRect().height}px`;
// we passed the taret so reset - we have to factor the target height in the calc
if (scrollY > targetY + this.ref.getBoundingClientRect().height) {
window.document.body.style.paddingBottom = "0px";
this.setState({ lockedIntoView: false });
}
} else {
// if we scrollign down and at the target, then lock
if (
scrollY > lastScrollY &&
(scrollY >= targetY &&
scrollY < targetY + this.ref.getBoundingClientRect().height)
) {
console.info("we locked");
this.setState({ lockedIntoView: true });
}
}
// update last scroll position to determine if we going down or up
lastScrollY = scrollY;
}
render() {
const { lockedIntoView, targetY } = this.state;
const fixed = lockedIntoView ? "lockedIntoView" : "";
return (
<div className="App">
<div className="bg-near-white min-vh-100 ">
<h1>First section</h1>
</div>
<div
ref={r => (this.ref = r)}
className={`vh-100 bg-blue pa0 ma0 ${fixed}`}
>
<h2>
When this is in full view of the window, it should remain fixed
until the window has scrolled the full length of the window
</h2>
</div>
<div style={{ height: "300vh" }} className="bg-near-white min-vh-100 ">
<h2>The next section</h2>
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim
ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat. Duis aute irure dolor in
reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla
pariatur. Excepteur sint occaecat cupidatat non proident, sunt in
culpa qui officia deserunt mollit anim id est laborum.
</p>
</div>
</div>
);
}
}
const rootElement = document.getElementById("root");
render(<App />, rootElement);