Unlock seamless workflows and faster delivery with our latest releases – get the details

Image Preview Field

By Kristel Voets

Preview images of your documents

components/ImagePreviewField.js

import React, { useMemo } from 'react';
import get from 'lodash.get';
import { withDocument } from 'part:@sanity/form-builder';
import Fieldset from 'part:@sanity/components/fieldsets/default';

import { urlFor, calculateRatio } from '../lib/image';

const ImagePreview = (props) => {
  const image = props.image;
  const options = props.options ?? {};
  const ratio = options.ratio ?? image?.ratio ?? props.ratio;

  const url = useMemo(() => {
    if (typeof image === 'object' && image?.asset?._ref) {
      const imageUrl = urlFor(image).auto('format').format('jpg');
      if (typeof ratio === 'string' && ratio.indexOf(':') > 0) {
        const [width, height] = calculateRatio(ratio);
        return imageUrl.width(width).height(height).url();
      } else {
        return imageUrl.maxWidth(1200).maxHeight(1200).fit('max').url();
      }
    }
  }, [image, ratio]);

  return (
    <div
      style={{
        width: '100%',
        height: '400px',
        paddingLeft: '6px',
        paddingRight: '6px',
      }}
    >
      {url && (
        <img
          src={url}
          style={{
            display: 'block',
            width: '100%',
            height: '100%',
            objectFit: 'contain',
          }}
        />
      )}
    </div>
  );
};

const ImagePreviewField = React.forwardRef((props, ref) => {
  const { document, level, type } = props;
  const property = get(type, ['options', 'imageProperty'], 'image');
  const limit = get(type, ['options', 'limit'], 2);

  const images = [].concat(get(document, property, [])).slice(0, limit);

  if (images.length > 0) {
    return (
      <Fieldset
        ref={ref}
        level={level}
        legend={type.title}
        description={type.description}
        columns={images.length > 1 ? 2 : 1}
        isCollapsible={true}
      >
        {images.map((image) => (
          <ImagePreview
            image={image}
            ratio={document.ratio}
            options={type.options}
          />
        ))}
      </Fieldset>
    );
  } else {
    return <div ref={ref}></div>;
  }
});

export default withDocument(ImagePreviewField);

lib/image.js

import trim from 'lodash.trim';

import imageUrlBuilder from '@sanity/image-url';

import client from 'part:@sanity/base/client';

export const builder = imageUrlBuilder(client);

export const urlFor = (source) => {
  return builder.image(source);
};

export function parseRatio(ratio) {
  if (typeof ratio === 'number') {
    return ratio;
  } else if (typeof ratio === 'string') {
    const [a, b] = ratio.split(':').map((p) => parseInt(trim(p), 10));
    return a / b;
  } else {
    return 3 / 2;
  }
}

export function calculateRatio(ratio, scale = 1) {
  const r = parseRatio(ratio);
  const a = r > 1 ? 1200 : 900;
  const b = r > 1 ? a / r : a * r;
  const width = round(scale * (r > 1 ? a : b));
  const height = round(scale * (r > 1 ? b : a));
  return [width, height, r];
}

function round(x) {
  return Math.ceil(x / 5) * 5;
}

example-field.js

export const PreviewField = {
  name: 'imagePreview',
  title: 'Preview',
  type: 'boolean', // could be any type
  inputComponent: ImagePreviewField,
  options: {
    imageProperty: 'images', // the property (single image or array of images)
    limit: 1, // optionally set the max. number of items to display
    ratio: '1:1', // optionally set a fixed ratio
  }
}

This field allows you to preview an image, or a number of images from an array.


The source field is set by the 'imageProperty’ option, which can refer to nested fields using dot-syntax.


You can set a fixed display ratio using the options, as well as the maximum number of images to display.


If the document or the particular image has a ‘ratio’ field (string, like: "3:2”), it will be used unless a fixed ratio has been specified.


Whenever a ratio is given, the image’s hotspot/cropping will be applied automatically.

Contributor