Adding Multiple Images with Portable Text

21 replies
Last updated: Apr 26, 2021
Hello, folks. I have some image/portable text issue I’m trying to resolve.

{
  name: 'content',
  title: 'Post content',
  type: 'array',
  of: [{ type: 'block' }, { type: 'image' }],
  description: 'Content of this post',
}
helps me add an image in my portable text, but this is just a single image. As in 1 by 1 (first image). Is there a way to do 2 by 1 (second image)? Or is this something I do when I render inside my code and not with the editor?
AI Update

Based on your question, what you're looking for is a custom block type rather than trying to modify the default image insertion. The single image behavior you're seeing is by design - when you add { type: 'image' } to your Portable Text array, it inserts one image at a time.

To achieve a 2-by-1 image layout (or any multi-image gallery), you'll want to create a custom block type. Here's how:

// First, define a custom image gallery block type in your schema
{
  name: 'imageGallery',
  title: 'Image Gallery',
  type: 'object',
  fields: [
    {
      name: 'images',
      title: 'Images',
      type: 'array',
      of: [{ type: 'image' }],
      validation: Rule => Rule.max(2) // Limit to 2 images for your 2x1 layout
    },
    {
      name: 'layout',
      title: 'Layout',
      type: 'string',
      options: {
        list: [
          { title: '2 by 1', value: '2x1' },
          { title: 'Single', value: '1x1' }
        ]
      }
    }
  ],
  preview: {
    select: {
      images: 'images',
      layout: 'layout'
    },
    prepare({ images, layout }) {
      return {
        title: `Image Gallery (${layout})`,
        subtitle: `${images?.length || 0} images`
      }
    }
  }
}

// Then add it to your content field
{
  name: 'content',
  title: 'Post content',
  type: 'array',
  of: [
    { type: 'block' },
    { type: 'image' }, // Keep this for single images
    { type: 'imageGallery' } // Add your custom gallery block
  ],
  description: 'Content of this post',
}

When it comes to rendering, you'll handle the layout in your frontend code. The Sanity editor just lets content creators choose which images to include - the 2x1 visual layout happens when you render it. For example, with React:

<PortableText
  value={content}
  components={{
    types: {
      imageGallery: ({ value }) => (
        <div className="grid grid-cols-2 gap-4">
          {value.images?.map((image, idx) => (
            <img key={idx} src={urlFor(image)} alt="" />
          ))}
        </div>
      )
    }
  }}
/>

The Portable Text documentation covers custom blocks in detail, and there's a great guide on adding custom blocks like YouTube embeds that follows the same pattern.

So to answer your question directly: the layout is something you control in your rendering code, but you need to create a custom block type in the schema to allow editors to add multiple images as a single unit.

Make a new schema for this called gallery, or imageList or something like that
user E
Done. What about the content? What type?
object
?
object makes the most sense, unless you want to query all your image galleries somewhere in one place
{
  name: '2by1imageList',
  title: '2 by 1 Image List',
  type: 'object',
  fields: [
    {
      name: 'columns',
      title: 'Columns',
      type: 'array',
      of: [{ type: 'image' }],
    },
  ],
},
Like that?
the beauty of the array is that it can contain 1 or 100's of images
what I like to do is make one image list schema, and then make a new field where you can select how the list should be displayed
Essentially a “list of images”
export default {
  name: 'gallery',
  type: 'object',
  title: 'Gallery',
  fields: [
    {
      name: 'images',
      type: 'array',
      title: 'Images',
      of: [
        {
          name: 'image',
          type: 'image',
          title: 'Image',
          options: {
            hotspot: true,
          },
          fields: [
            {
              name: 'alt',
              type: 'string',
              title: 'Alternative text',
            },
          ],
        },
      ],
      options: {
        layout: 'grid',
      },
    },
    {
      name: 'display',
      type: 'string',
      title: 'Display as',
      description: 'How should we display these images?',
      options: {
        list: [
          { title: 'Stacked on top of eachother', value: 'stacked' },
          { title: 'In-line', value: 'inline' },
          { title: 'Carousel', value: 'carousel' },
        ],
        layout: 'radio', // <-- defaults to 'dropdown'
      },
    },
  ],
  preview: {
    select: {
      images: 'images',
      image: 'images.0',
    },
    prepare(selection) {
      const { images, image } = selection;

      return {
        title: `Gallery block of ${Object.keys(images).length} images`,
        subtitle: `Alt text: ${image.alt}`,
        media: image,
      };
    },
  },
};
That should get you started 😉
the preview part is a bit hacky, didn’t get around to making it better yet
the beauty of this approach is that you can change display of your images without having to re-upload or re-select or change schema types…
😂 The future is bright. Thank you so much. Let me work with this
you’re welcome
I’d just use the display key to toggle the display. This is so so helpful!
I should post this on the snippets &amp; schemas site 🙂
You should. It’s really helpful. I realise I could also specify image size and display it that way instead of messing with the dimension
Oh yes, you can add a field “shape” and set it to landscape/square/portrait, or even use numeric values if you’re brave enough
Exactly!
Finally got it to work. 😄
Very nice!

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?