• 2019-04-02
  • React
  • CSS Animations
  • Open Source

Experiment Using Compound Components With React Fullscreen Image

Building react-fullscreen-image using compound components, hooks, and CSS transforms & opacity.

While helping @alexcarey.s build a simple portfolio site, we found a need to create a full screen view of a few secondary portfolio images. So I thought why not build a little library from scratch and try a compositional technique that I haven't before, with compound components.

Here's how it looks with a grid of images:

alexcarey.co portfolio demo

In this post I will briefly walk through how I used compound components, hooks, and CSS transforms to get the library to where it is currently at. The full repository is on Github and downloadable on npm. A live demo can be found on CodeSandbox as well!

We'll cover:

  1. UI/UX requirements
  2. Why Compound Components
  3. CSS Transform & Opacity For Image Scaling

UI/UX Requirements

We want to render a list of images, in any order and depth in the component tree.

When an image is clicked, we want that image to scale from its current position to the fullscreen view.

Once it's in its fullscreen view, we want to be able to navigate using keyboard arrows or arrow buttons to the previous and next images.

Finally, when we exit out of the image (using the escape key, exit button, or an outerclick) we'd like the current focused image to animate down to the position where it is supposed to be.

For how to use the API, I wanted to make the developer experience as easy as possible — a simple plug and play idea:

import { ImageGroup, Image } from 'react-fullscreen-image'

function App() {
  return (
    {/* Wrap any number of <Image /> inside of an <ImageGroup /> */}
    <ImageGroup>
      <ul>
        {/* Replace any <img /> tags with <Image /> */}
        {images.map(image => (
          <li key={image}>
            <Image src={image.src} alt={image.alt} />
          </li>
        ))}
      </ul>
    </ImageGroup>
  )
}

Why Compound Components

We want to render any number of images, at any depth level of the component tree, where each each Image shares some implicit state. Each Image will need to know a few things:

  • Am I currently focused? If I am, then we'll need to determine how we're going to animate in.
  • Are we in fullscreen mode? If we're not, that means we need to animate the image in. If we are, then we'll need to immediately show the image instead, since we've already animated to fullscreen mode.

ImageGroup will manage the state for all of this, and pass it down to Image components as props. By using a recursive implementation of React.Children.map, we're be able to capture every Image component no matter where it's at in the component tree.

The image resizing calculation happens within Image. Image's useEffect hook will update each time its props update. ImageGroup will essentially say one of the following:

  • "Hey Image, you're currently in focus. You need to animate in."
  • "Hey Image, you're currently in focus, but you don't need to animate in since we're already in fullscreen mode"
  • "Hey Image, you're not in focus, and the user just exited fullscreen mode so you should animate out"
  • "Hey Image, you're not in focus anymore, but since the user just navigated to another image, we're still in fullscreen mode. In this case, just immediately go back to your original position"

Based on these cases, Image will handle each appropriately. This brings us to why I decided to implement fullscreen using CSS transform and opacity.

CSS Transform & Opacity For Image Scaling

Currently, what Image does is take a native <img /> tag and returns a component that wraps the <img /> like so:

// ...

return (
  <div className="fullscreen-container">
    <button className="fullscreen-btn">
      <div className="fullscreen-image--original">
        <img src={src} alt={alt} style={style} />
      </div>
      <div className="fullscreen-image--large">
        <img src={src} alt={alt} style={style} />
      </div>
    </button>
  </div>
)

You can see that we return two <img />s. Initially, the large image will be hidden with opacity: 0. When Image is clicked, we calculate how much to scale the image to fill the viewport, change the opacity, and take the large image and scale it to the calculated value.

Some fullscreen implementation I've seen, like Medium's, animates the image's height/width, butj a transform implementation is cheaper and I think quite simpler since the browser will handle all the calculations for when the image needs to scale down to its original position (with a simple transform: translate3d(0, 0, 0).

**I think in the next version of react-fullscreen-image, we can add an extra prop to Image as hdImgSrc where the user can specify a higher quality version of the image for the fullscreen version.

Comments and Suggestions?

I'm pretty new to building libraries, working with Typescript, and creating packages. If you have suggestions/tips on better implementation methods, testing, or just code in general I'd love to hear from you!

Previous
  • React
  • How To

Dynamic List Editor With React

Up Next
  • React
  • Open Source

Building a React component that handles all visual state for menus and lists