Creating Interactive Product Pages With React and Cloudinary

Because today’s consumers expect a level of product customization they purchase online, e-commerce sites must support more personalization, a key to which is adding features to product pages, also called order pages. That’s where shoppers can customize the products they want to buy by changing product properties, such as size, color, delivery means, and quantity.

User-friendly UX demands that when shoppers make purchases, they receive visual feedback. For example, if someone is buying a shirt in red, the product page should account for that by updating the shirt’s image to a red variant. Given how difficult it is to change the color of non-SVG images, implementing such a feature can be daunting. Fortunately, Cloudinary, a cloud-based solution for managing and delivering rich media, including images and videos, makes it a breeze.

This post steps you through the process of leveraging Cloudinary to build a demo app with these three capabilities for product pages:

  • Varying image sizes: Cloudinary can seamlessly deliver product images in multiple sizes (main image; thumbnails; hi-resolution, zoomed-in images). All you need to do is add the sizes to the URL, after which Cloudinary dynamically generates the various images.
  • Varying colors: Some products come in multiple colors, the images for which are typically individual ones that all need to be uploaded and processed. With Cloudinary, you can change the color of a product by calculating how much to adjust the RGB channels of the original color to arrive at the desired color. This approach allows infinite scaling, enabling you to switch to any color in the spectrum.
  • Custom text: Many retailers offer features for personalization, such as embroidering, adding logos, and designing your own products. Cloudinary can overlay text and images on top of a shirt, for example, and can even make it look photorealistic with displacement mapping.

This is what the demo app looks like:

Here are the topics:

  • Setting Up the Environment
  • Solving Problem 1: Varying Image Sizes
  • Solving Problem 2: Varying Colors
  • Solving Problem 3: Custom Text
  • Trying It Out

Setting Up the Environment

Cloudinary integrates well with all front-end frameworks so feel feel to use the JavaScript core library or a specific framework-wrapper library. For the demo app in this article, set up the environment with React, as follows:

# 1. Install create-react-app globally.
npm install -g create-react-app
# 2. Create a new app.
create-react-app cloudinary-retail-page
# 3. Install the Cloudinary React library.
npm install --save cloudinary-react

For simplicity, all the sample code resides in the src/App.js folder.

Solving Problem 1: Varying Image Sizes

Above are two sets of images: main and thumbs (short for thumbnails). The main image is for shoppers; the thumbs, for interactions. Obviously, the selected thumb is the same as the main image, and you can deliver them in either of these two most common ways:

  • Manually create multiple images for a given product. However, this approach is not scalable given the large amount of products on most e-commerce sites.
  • Resize high-resolution images to fit the main or thumb section by means of CSS width and height properties. That’s the wrong thing to do because if an image has three variations and if each of them takes up 800 KB, you end up with the following: 1 main (800 KB) + (1 thumb (800 KB) x 3 models) = 3,200 KB (3.2 MB)

By specifying the size you prefer during delivery through image transformation with Cloudinary, you would have only one image on your Cloudinary server and can request a particular size for delivery. See this example code:

import React, { Component } from 'react';
import {Image, CloudinaryContext, Transformation} from 'cloudinary-react';

const ImageTransformations = ({width, selectedShirt}) => {
    return (
        <Image publicId={selectedShirt.main+'.jpg'}>
            <Transformation width={width} crop="scale" />
        </Image>
    );
};

class App extends Component {

    constructor(props) {
        super(props);
        const defaultShirt = {id: 1, main: 'shirt_only'};
        this.state = {
            shirts: [
                defaultShirt,
                {id: 2, main: 'laying-shirt'},
                {id: 3, main: 'hanging_t-shirt'}
            ],
            selectedShirt: defaultShirt,
        };
    }

    selectShirt(thumb) {
        this.setState({selectedShirt: thumb}, _ => this.forceUpdate())
    }

    render() {

        return (
          <div className="App">
              <CloudinaryContext cloudName="<YOUR_CLOUD_NAME>">
                  <div id="imageDemoContainer">
                      <div id="mainImage">
                          <ImageTransformations
                              width="600"
                              rgb={rgb}
                              selectedShirt={this.state.selectedShirt}
                              text={this.state.text} />
                      </div>
                      <div id="imageThumbs">
                          <ul id="thumbs">
                              {this.state.shirts.map(thumb => {
                                 return (
                                 <li className={thumb.main === this.state.selectedShirt.main ? 'active': ''} onClick={this.selectShirt.bind(this, thumb)} key={thumb.id}>
                                     {/*<Image publicId={thumb.main}>*/}
                                         {/*<Transformation width="75" crop="scale" />*/}
                                     {/*</Image>*/}
                                     <ImageTransformations
                                         width="75"
                                         rgb={rgb}
                                         selectedShirt={thumb}
                                         text={' '} />
                                 </li>
                                 )
                              })}
                          </ul>
                      </div>
                  </div>
              </CloudinaryContext>
          </div>
        );
    }
}

export default App;

Cloudinary’s React library exposes four components:

  • Image: Delivery of images, each with a public ID (publicId).
  • Video: Delivery of videos, each with a public ID (publicId).
  • Transformation: Transformations of images and videos.
  • CloudinaryContext: Wrapping of multiple instances of Image and Video under your cloud name, which Cloudinary assigns to you once you’ve signed up for free.

Here’s what transpires:

  1. The React state holds an array of image public IDs, `shirts`, that are on the Cloudinary server.
  2. You iterate over that array of shirts and request images of width 75 px. with the custom `ImageTransformations` component. Those images are then displayed as thumbs.
  3. On a click of a thumb, `ImageTransformations` renders the main image of width 600 px., which makes for an optimized solution, as shown here:

1 main (800 KB) + 1 thumb (100 KB ) x 3 models = 1,100 KB (1.1 MB)

3,200 KB  – 1,100 KB = 2,100 KB (2.1 MB)

That’s a saving of more than 60% of extra kilobytes, thanks to Cloudinary.

Solving Problem 2: Varying Colors

When shoppers choose a color for a product, what retailers usually do is create a clickable color palette and replace the images with the product image that sports the selected color. Such a practice does not scale, however, because a shopper might prefer a color that does not match any of the product colors on your page. Ideally, a given product would have countless colors, or shoppers would be able to customize the product’s color after purchase.

With Cloudinary, you can adjust the RGB channels of the original color with simple transformation—and with only one image as the source. See this code:

<Transformation effect="red:255" />
<Transformation effect="blue:255" />
<Transformation effect="green:255" />

You can then apply that code to the previous example, like this:

...
import { SketchPicker } from 'react-color';

const ImageTransformations = ({width, rgb, selectedShirt, text}) => {
    return (
        <Image publicId={selectedShirt.main+'.jpg'}>
            <Transformation width={width} crop="scale" />
            <Transformation effect={'red:'+((-1+rgb.r/255)*100).toFixed(0)} />
            <Transformation effect={'blue:'+((-1+rgb.b/255)*100).toFixed(0)} />
            <Transformation effect={'green:'+((-1+rgb.g/255)*100).toFixed(0)} />
            <Transformation underlay={selectedShirt.underlay}  flags="relative" width="1.0" />
            <Transformation overlay={selectedShirt.overlay}  flags="relative" width="1.0"  />
        </Image>
    );
};

class App extends Component {

    constructor(props) {
        super(props);
        const defaultShirt = {id: 1, main: 'shirt_only', underlay: 'model2', overlay: ''};
        this.state = {
            shirts: [
                defaultShirt,
                {id: 2, main: 'laying-shirt', underlay: '', overlay: ''},
                {id: 3, main: 'hanging_t-shirt', underlay: '', overlay: 'hanger'}
            ],
            text: ' ',
            selectedShirt: defaultShirt,
            background: {rgb:{r:255,g:255,b:255}}
        };
    }

    handleColorChange(color) {
        // Updates color
        this.setState({ background: color }, _ => this.forceUpdate());
    };

    selectShirt(thumb) {
        // Updates main image
        this.setState({selectedShirt: thumb}, _ => this.forceUpdate())
    }

    render() {
        const rgb = this.state.background.rgb;

        return (
          <div className="App">
              <CloudinaryContext cloudName="christekh">
                 <div id="demoContainer">
                      <div id="header">
                          <a href="http://cloudinary.com/">
                              <img width="172" height="38" src="http://res-1.cloudinary.com/cloudinary/image/asset/dpr_2.0/logo-e0df892053afd966cc0bfe047ba93ca4.png" alt="Cloudinary Logo" />
                          </a>
                          <h1>Product Personalization Demo</h1>
                      </div>
                  </div>
                  <div id="imageDemoContainer">
                      <div id="mainImage">
                          <ImageTransformations
                              width="600"
                              rgb={rgb}
                              selectedShirt={this.state.selectedShirt}
                              text={this.state.text} />
                      </div>
                      <div id="imageThumbs">
                          <ul id="thumbs">
                              {this.state.shirts.map(thumb => {
                                 return (
                                 <li className={thumb.main === this.state.selectedShirt.main ? 'active': ''} onClick={this.selectShirt.bind(this, thumb)} key={thumb.id}>
                                     {/*<Image publicId={thumb.main}>*/}
                                         {/*<Transformation width="75" crop="scale" />*/}
                                     {/*</Image>*/}
                                     <ImageTransformations
                                         width="75"
                                         rgb={rgb}
                                         selectedShirt={thumb}
                                         text={' '} />
                                 </li>
                                 )
                              })}
                          </ul>
                      </div>
                  </div>
                  <div id="demoInputContainer">
                      <div className="inputSelections">
                          <h2>Shirt Color:</h2>
                          <SketchPicker
                              color={ this.state.background.hex }
                              onChangeComplete={ this.handleColorChange.bind(this) }
                          />
                      </div>
                  </div>
              </CloudinaryContext>
          </div>
        );
    }
}

export default App;

You’ve now extended the app, as follows:

  • react-color library is a color library with many options. With the option SketchPicker, you select a color and set the background state with that color by means of handleColorChange.
  • You set the rg, and b values of the image with the Transformation component.
  • The RGB values are negative because Cloudinary’s redblue, and green parameters adjust the respective color channel by percentage, and the color white in RGB is the maximum value of 256,256,256. The only way to go is down from white, hence the negative adjustments.
  • To display the two images, one of the model wearing a shirt and the other of a hanger, you apply the underlay and overlay transformation, respectively.

Note: The code manually updates the view with component.forceUpdate() because component.setState() is asynchronous, which causes delays in reflecting the changes.

Solving Problem 3: Custom Text

You can add text to images with the Cloudinary overlay feature, which is a standard service offered by companies that print custom text on fabrics. The code can’t be simpler:

<Transformation overlay="text:Roboto_30Scotch.io" />

Next, add the text Scotch.io in 30-px. Roboto font. Alternatively, add a text field to collect the text and update the image with the text on receipt of a keystroke, as follows:

import React, { Component } from 'react';
import {Image, CloudinaryContext, Transformation} from 'cloudinary-react';
import { SketchPicker } from 'react-color';
import './App.css';

const ImageTransformations = ({width, rgb, selectedShirt, text}) => {
    return (
        <Image publicId={selectedShirt.main+'.jpg'}>
            <Transformation overlay={'text:Roboto_30:'+text} flags="relative" gravity="center" />
        </Image>
    );
};

class App extends Component {

    constructor(props) {
        super(props);
        this.state = {
            text: ' ',
          ...
        };
    }

   ...

    handleTextChange(event) {
        this.setState({text: event.target.value}, _ => this.forceUpdate())
    }

    render() {
        const rgb = this.state.background.rgb;

        return (
          <div className="App">
              <CloudinaryContext cloudName="christekh">
                  <div id="imageDemoContainer">
                  ..
                  </div>
                  <div id="demoInputContainer">
                      ...
                      <div className="inputSelections">
                          <h2>Text:</h2>
                          <input className="form-control" type="email" placeholder="Enter text" value={this.state.text} onChange={this.handleTextChange.bind(this)} />
                      </div>
                  </div>
              </CloudinaryContext>
          </div>
        );
    }
}

export default App;

We’ve truncated the above code sample so you can see the entire process.

Trying It Out

Using Cloudinary on your e-commerce site greatly eases the life of everyone on the creative team, opening up new opportunities that wouldn’t be available otherwise. Get started by signing up for free on the Cloudinary website.