UNPKG

popmotion-pose

Version:

A declarative animation library for HTML and SVG

258 lines (185 loc) 7.72 kB
--- title: "Tutorial: Medium-style image zoom" description: How to make Medium-style image zooming with Pose for React category: react --- > React Pose has been **deprecated** in favour of [Framer Motion](https://framer.com/motion). [Read the upgrade guide](https://www.framer.com/api/motion/migrate-from-pose/) # Tutorial: Medium-style image zoom [Medium](https://medium.com) have an beautiful zoom effect on their images. When clicked, they pop out of the page as a white background fades in behind them. Then, if clicked again, or if a user scrolls away, they pop back into place. Take a look: <iframe width="600" height="400" src="https://www.youtube.com/embed/Y2gd1ILCoYA" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe> In this tutorial, we'll learn how to achieve this same effect using Pose for React. <TOC /> ## Setup To get started, fork this [CodeSandbox template](https://codesandbox.io/s/y2k00vx22x). It contains a mock article that contains a couple of images. These are being rendered via the component we're going to work on, `ZoomImage`. Open `components/ZoomImage.js`, and let's get started! ## State First, we need to create `state` so we know whether the image is zoomed or not. At the top of the `ZoomImage` class, add the following: ```javascript state = { isZoomed: false }; ``` Of course, this state is useless on its own. We're going to need a couple of functions to set the zoom status. On the next line, add the following `zoomIn` and `zoomOut` methods: ```javascript zoomIn() { this.setState({ isZoomed: true }); } zoomOut() { this.setState({ isZoomed: false }); } ``` Finally, we want to toggle the zoomed state when someone clicks the image container (as this will also later contain the white background): ```javascript <div class="image-frame" onClick={() => this.state.isZoomed ? this.zoomOut() : this.zoomIn()} style={{ width: imageWidth, height: imageHeight }} > ``` Now, when the image is clicked, the component will change zoom status. But we're not responding to this in our `render` function. Let's make some animations! ## Image zoom animation When the image zooms in, it needs to animate from its place in the document, smoothly into the center of the screen. To do this, we're going to use Pose's FLIP capabilities. You can read the gritty details about FLIP in this [blog post by Paul Lewis](https://aerotwist.com/blog/flip-your-animations/). In essence, it's a way of *performantly* animating between two states that would otherwise be expensive, for instance where `position`, `top`, `width`, or other layout-changing properties have changed. In Pose, you simply have to add `flip: true` to a pose, and it'll automatically perform the usually complicated steps to perform this animation. Import Pose for React: ```javascript import posed from 'react-pose'; ``` Now make a posed `img` component: ```javascript const Image = posed.img(); ``` We're going to provide the component two poses, one for each zoom state: `zoomedIn` and `zoomedOut`. Our `zoomedIn` pose is going to set `position: fixed` and every positional prop to `0`. This will pop the content out of the layout and lock it to the viewport. In our `styles.css` file, `img` has a style of `margin: auto` which centers the image when it's being stretched across the screen in this way. ```javascript const Image = posed.img({ zoomedIn: { position: 'fixed', top: 0, left: 0, bottom: 0, right: 0, flip: true } }); ``` `zoomedOut` sets `position: static` to pop it back into the DOM, as well as setting `width` and `height` to `auto` to make it fill its layout container: ```javascript const Image = posed.img({ zoomedIn: { position: 'fixed', top: 0, left: 0, bottom: 0, right: 0, flip: true }, zoomedOut: { position: 'static', width: 'auto', height: 'auto', flip: true } }); ``` We've now got our posed `img` component fully configured. Replace the `img` component in the `render` function with it: ```javascript <Image {...props} /> ``` To animate `Image` between the two poses, we need to provide it a `pose` property. At the top of the `render` function, set our `pose`: ```javascript const { isZoomed } = this.state; const pose = isZoomed ? 'zoomedIn' : 'zoomedOut'; ``` And provide it to `Image`: ```javascript <Image pose={pose} {...props} /> ``` Now, when we click our image, it zooms in and out! I find the automatically generated animation a little bouncy for this purpose. We can define a new `transition` with a `ease` curve generated at [Lea Verou's cubic bezier generator](http://cubic-bezier.com/#.08,.69,.2,.99). ```javascript const transition = { duration: 400, ease: [0.08, 0.69, 0.2, 0.99] }; ``` Provide this as a `transition` prop to both poses, and the animation becomes a little slicker. ## Background animation That's the (usually) difficult bit out of the way. It's looking pretty good but the Medium example fades a background in behind the image as it zooms in and out. Make a new posed component called `Frame`: ```javascript const Frame = posed.div(); ``` In our `styles.css` add a new rule for `.frame`. We're going to make the background of this frame white, and set `translateZ(0)` to ensure its fade animation is hardware-accelerated: ```css .frame { position: fixed; top: 0; left: 0; right: 0; bottom: 0; display: none; background: white; transform: translateZ(0); } ``` And now add `Frame` as a sibling of `Image`, also passing it the `pose` prop: ```javascript <Frame pose={pose} class="frame" /> <Image pose={pose} {...props} /> ``` Now we can animate it! We just want to fade the overlay in and out, so first add those poses: ```javascript const Frame = posed.div({ zoomedIn: { opacity: 1 }, zoomedOut: { opacity: 0 } }); ``` By itself this won't do anything, as we've got `display` set to `none` in the CSS. For this we can use the `applyAtStart` and `applyAtEnd` props. They allow you to define styles to set at the start and at the end of the pose transition, respectively. ```javascript const Frame = posed.div({ zoomedIn: { applyAtStart: { display: 'block' }, opacity: 1 }, zoomedOut: { applyAtEnd: { display: 'none' }, opacity: 0 } }); ``` Now your background will fade in and out behind the image as it zooms in! ## Scroll to zoom out The original Medium image zoom has a nice feature where if a user starts scrolling, the image zooms out back into its original place. We can accomplish the same thing by adding a `'scroll'` event listener to `zoomIn`: ```javascript zoomIn() { window.addEventListener('scroll', this.zoomOut); this.setState({ isZoomed: true }); } ``` By itself, this isn't going to work. When `this.zoomOut` is called, it'll be in the execution context of the event caller rather than our React component. We can bind `zoomOut` to our component by changing it to an arrow function: ```javascript zoomOut = () => { this.setState({ isZoomed: false }); }; ``` Finally, we need to remove the event listener when a user does zoom out: ```javascript zoomOut = () => { window.removeEventListener('scroll', this.zoomOut); this.setState({ isZoomed: false }); }; ``` ## Conclusion Here's our finished example: <CodeSandbox height="700" id="rrjx477w3n" /> There's plenty of fun things you can do to improve accessibility and aesthetics. Have a think about: - Closing the image via the `esc` key - Changing the background animation. You could even incorporate SVGs. - Adding a "scroll delay" where a user has to scroll a minimum distance before we close the image. - Changing the cursor to show a zoom in or zoom out icon.