Create a Post Editor Similar to Twitter with a Responsive Images Grid with tailwindcss
Aymen kani|January 5th 2023

In this tutorial, we will create a React component for a social media post editor that allows the user to write posts and display a grid of selected images while preserving their aspect ratio. The component also allows the user to remove images from the grid.

We're going to use Tailwindcss and nextjs in this tutorial so make sure to install and configure your project following Tailwind css with Nextjs guide.

To get started, let's import the necessary dependencies and create a functional component called PostEditorImages:

javascript

import React, { useEffect, useState } from 'react';

const PostEditorImages = ({ images, removeImage }) => {
  // component code goes here
}

Our component will accept two props:

  • images: an array of images of type File, representing the images to be displayed in the grid.
  • removeImage: a function that removes an image from the images array and updates the state of the parent component.

Next, let's set up some state variables that we will use to control the layout of the image grid:

javascript

const [imagesContainerGrid, setImagesContainerGrid] = useState("grid-cols-2 grid-rows-2");
const [aspectRatios, setAspectRatios] = useState({});

The imagesContainerGrid state variable will hold a string of CSS class names that define a grid template columns of 2 and rows of 2. The aspectRatios state variable will hold an object that maps the index of each image to its aspect ratio.

Now, let's write a useEffect hook that updates the imagesContainerGrid state variable based on the number of images:

javascript

useEffect(() => {
  setImagesContainerGrid(images.length >= 3 ? "grid-cols-2 grid-rows-2 h-[300px]" : images.length > 1 ?"grid-cols-2" : "grid-cols-1");
}, [images]);

This useEffect hook will run every time the images prop changes. If there are three or more images, it will set the imagesContainerGrid state variable to a string that specifies a grid with two columns and two rows, with a fixed height of 300px. If there are two or more images, it will set the imagesContainerGrid state variable to a string that specifies a grid with two columns. If there is only one image, it will set the imagesContainerGrid state variable to a string that specifies a grid with one column.

Now, let's write a useEffect hook that calculates the aspect ratio of each image and updates the aspectRatios state variable:

javascript

useEffect(() => {
      images.forEach((image, index) => {
        const reader = new FileReader();
    
        reader.onload = function(event) {
          const data = event.target.result;
          const img = new Image();
          img.src = data;
          img.onload = function() {

            const gcd = (a, b) => (b === 0 ? a : gcd(b, a % b));
            const numerator = img.width / gcd(img.width, img.height);
            const denominator = img.height / gcd(img.width, img.height);
            setAspectRatios(ratios => ({ ...ratios, [index]: `${numerator}/${denominator}` }));
          };
        };
    
        reader.readAsDataURL(image);
      });
}, [images]);

This code iterates over an array of images and calculates the aspect ratio of each image. It does this by:

  1. Creating a FileReader object for each image, which allows the code to read the contents of the file as a data URL.
  2. Setting an onload event handler for the FileReader object, which is called when the file has been successfully read. The event handler creates a new image object, sets its src property to the data URL, and sets an onload event handler for the image object.
  3. The onload event handler for the image object calculates the aspect ratio of the image by dividing the width by the greatest common divisor of the width and height, and dividing the height by the greatest common divisor. It then updates the aspectRatios state variable by creating a new object that spreads the existing aspect ratios and adds a new key-value pair for the current image's index and aspect ratio.
  4. Finally, the code calls the readAsDataURL method on the FileReader object, passing in the image file as an argument. This starts the process of reading the file as a data URL.

Finally, let's render the image grid in the component's JSX:

jsx

return (
  <div className={` [grid-area:images] grid gap-1 ${imagesContainerGrid}  `}>
    {/* display the selected images */}
    {images.length > 0 &&
      [...images].map((image, index) => (
        <div
          key={image.name}
          className={`relative rounded-md`}
          style={{
            backgroundImage: `url(${URL.createObjectURL(image)})`,
            backgroundSize: 'cover',
            backgroundPosition: 'center center',
            backgroundRepeat: 'no-repeat',
            width: '100%',
            height: '100%',
            aspectRatio: aspectRatios[index]
          }}
        >
          <button
            className="absolute flex items-center justify-center top-0 text-sm right-0 p-2 text-white rounded-full w-7 h-7 shadow-md bg-black bg-opacity-60 hover:bg-opacity-100"
            onClick={() => removeImage(index)}
          >
            <span>X</span>
          </button>
        </div>
      ))
    }
  </div>
);

This JSX creates a div of grid-area:images that will be defined in the parent component , with the grid class and the imagesContainerGrid class, which defines the layout of the image grid. Inside the div, it maps over the images array and renders a div for each image. Each image div has a background image set to the image file, and the aspectRatio style property set to the aspect ratio of the image. The div also has a remove button that calls the removeImage function and passes in the index of the image to be removed.

That's it! With these simple steps, we have created a React component that displays a grid of images selected by the user, and allows the user to remove images from the grid.

To use the component, we need to create the SocialMediaTextEditor component which represents the text editor for the the social media app:

javascript

const SocialMediaTextEditor = () => {
  const [text, setText] = useState('')// state to store the text;
  const [images, setImages] = useState([]); // state to store the images


  const imageContainerRef = useRef()

  const handleTextChange = (event) => {
    setText(event.target.value);
  };

  const handleImageChange = (event) => {
    // update the images state by concatenating the selected files with the existing ones
    setImages([...images, ...event.target.files]);
  };

  const removeImage = (index) => {
    // create a new array with the selected image removed
    const newImages = [...images];
    newImages.splice(index, 1);
    setImages(newImages);
  };

  return (...)

Here, we have the handleTextChange to save the latest value of the text field, the handleImageChange and removeImage functions are responsible for adding and removing images from the images list.

Now lets define and style the post editor component using grid template areas with tailwindcss:

jsx

return (
   <div
  className={` rounded-lg p-4
    grid gap-y-1
    grid-cols-[40px_5px_1fr]
    grid-rows-[40px_1fr_minmax(0,_max-content)_40px]
    [grid-template-areas:'profile_p1_post'_'empty2_p3_post'_'empty3_p3_images'_'empty4_p4_postActions']`}

>
{/*  */}
  </div>
)

The SocialMediaTextEditor component contains a profile area where you can add a profile image. The post area represents the text field where the user can write their post. The p1, p3, and p4 represent a 5px gap that is defined in the grid-cols-[40px_5px_1fr] layout. The postActions area holds the upload images button and the post button. The emptyX fills in the profile area in all grid rows except the first row, giving the component a similar style to the Twitter post editor.

Now, let's define the JSX code for the profile, text field, and displayed images:

jsx

...
ā    {/* profile area */}
  <div className="[grid-area:profile] rounded-full bg-red-500" />
  {/* post area */}
  <textarea
    className="w-full [resize:none] [grid-area:post] h-fit p-2 rounded-lg text-gray-500  focus:outline-none"
    placeholder="Write your post here..."
    value={text}
    onChange={handleChange}
  />

  {/* display selected images */}
  <PostEditorImages images={images} removeImage={removeImage} />
ā 
...

Now add the jsx code that will allow us to select images from the local system :

jsx

...
{/* display selected images */}
 <PostEditorImages images={images} removeImage={removeImage} />

  <div className="[grid-area:postActions] flex flex-row justify-between">
    <div className="justify-self-start flex flex-row gap-2">
      {/* add the input element to select images */}
      <label htmlFor="image-upload" className="cursor-pointer">
        <FaImage size={20} color="rgb(31 41 55)" />
      </label>
      <input
        id="image-upload"
        type="file"
        accept="image/*"
        multiple
        onChange={handleImageChange}
        className="hidden"
      />
    </div>
    <button
        className="bg-red-500 justify-self-end hover:bg-red-700 text-white font-bold py-2 px-4 rounded-full"
    >
        Post
    </button>
 </div>
ā 
...

You can import the FaImage component from react-icons package.

That's it! You now have a fully functional social media post editor component with a responsive display images grid and a style similar to the Twitter post editor. Of course, you still need to create the logic for uploading the images to the server.

I hope this tutorial was helpful in showing you how to display and manage a grid of images in a React app. Happy coding!

aspect ratiotwitter-like post editorreact.jsimagestailwindcssresponsive images grid