LANARS

How to Build a Tool for Generating Linear and Radial Gradients with React: Complete Tutorial, Part 3

How to Build a Tool for Generating Linear and Radial Gradients with React: Complete Tutorial, Part 3
Time to read
15 min
Section
Share
Subscribe
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

Hello! Welcome back to part three of our tutorial for excited developers. In this tutorial, We'll use React for our app, utilize the react-colorful library for color manipulation, add smooth animations with framer-motion, ensure type safety with TypeScript, and style our app using Sass.

Let's outline the key topics we'll cover in this tutorial:

  • Start the project and set up React and Vite, install necessary dependencies
  • Add custom fonts and other media
  • Include types and constants.
  • Create a basic layout with a header and logo.
  • Add a feature to switch between light and dark themes.
  • Make gradients using a Gradient Generator.
  • Create the Color Picker Component using the react-colorful library
  • Let users copy gradient CSS code.
  • Add animation to your gradients using framer-motion.

Hold on to your keyboards, guys!

Copying gradient CSS code to the clipboard

The GradientCode component will display the CSS code for the gradient and provide functionality to reset the gradient and copy the CSS code to the clipboard.

/* src/components/GradientGenerator/GradientCode/index.tsx */

import React, { useState, useRef, useEffect } from 'react';
//...

const defaultLinesNumber = 6;

interface GradientCodeProps {
  gradient: string;
  resetGradient: () => void;
  addNewMessage: (newMessage: Omit<GeneralMessage, 'id'>) => void;
}

const GradientCode: React.FC<GradientCodeProps> = ({
  gradient,
  resetGradient,
  addNewMessage,
}) => {
  const [linesNumber, setLinesNumber] = useState(defaultLinesNumber);
  const codeContainerRef = useRef<HTMLSpanElement>(null);

  const handleCalculateLinesNumber = () => {
    const codeHeight = codeContainerRef.current?.offsetHeight ?? 100;
    const neededLinesNumber = Math.ceil(
      codeHeight / parseInt(gradientCodeLineHeight, 10)
    );

    setLinesNumber(
      neededLinesNumber > defaultLinesNumber
        ? neededLinesNumber
        : defaultLinesNumber
    );
  };

  useEffect(() => {
    document.fonts.ready.then(() => handleCalculateLinesNumber());
    window.addEventListener('resize', handleCalculateLinesNumber);

    return () =>
      window.removeEventListener('resize', handleCalculateLinesNumber);
  }, []);

  useEffect(() => {
    handleCalculateLinesNumber();
  }, [gradient]);

  const handleCopyGradientCode = () => {
    navigator.clipboard.writeText(`background: ${gradient};`);
    addNewMessage({
      text: 'CSS code has been copied',
      lifeTime: messageLifeTime,
    });
  };

  return (
    <section className="gradient-code">
      <div className="gradient-code__top">
        <div className="gradient-code-lines">
          {Array.from(Array(linesNumber).keys()).map((number) => (
            <p key={number} className="gradient-code-line">
              {number + 1}
            </p>
          ))}
        </div>

        <div className="gradient-code-label">CSS</div>

        <div className="gradient-code-preview">
          <span ref={codeContainerRef}>
            <span className="gradient-code-preview__key">background</span>:
            <span className="gradient-code-preview__value">
              {` ${gradient};`}
            </span>
          </span>
        </div>
      </div>

      <div className="gradient-code__bottom">
        <button
          type="button"
          className="gradient-code__reset-btn"
          onClick={resetGradient}
        >
          reset
        </button>

        <button
          type="button"
          className="gradient-code__copy-btn"
          onClick={handleCopyGradientCode}
        >
          Copy CSS
        </button>
      </div>
    </section>
  );
};

export default GradientCode;

 

Import and use it within the GradientGenerator component.

/* src/components/GradientGenerator/index.tsx */

import React, { useCallback, useEffect, useState } from 'react';
import GradientCode from './GradientCode';
//…

interface GradientGeneratorProps {
  addNewMessage: (newMessage: Omit<GeneralMessage, 'id'>) => void;
}

const GradientGenerator: React.FC<GradientGeneratorProps> = ({
  addNewMessage,
}) => {
  const [gradient, setGradient] = useState<string>('');
      /* ... */
  const resetGradient = () => {
    initGradient(defaultGradient);
  };
      /* ... */
  return (
    <div className="gradient-generator">
      /* ... */
      <GradientCode
        gradient={gradient}
        resetGradient={resetGradient}
        addNewMessage={addNewMessage}
      />
    </div>
  );
};

export default GradientGenerator;

 

Create a modal that can display messages to the user

When the user clicks the "Copy CSS" button in the GradientCode component, it will copy the gradient code to the clipboard and display a message indicating that the code has been copied.

We will create a modal window that can display messages to the user in a user-friendly way, such as notifications or alerts. When you call the addNewMessage function, it adds a message to the MessageContainer, and the messages will automatically disappear after their specified lifeTime.

/* src/components/MessageBox/index.tsx */

import React, { useEffect, useRef, useCallback } from 'react';
import { GeneralMessage } from '@shared/types/interfaces';
import CloseIconBig from '@assets/svg/close-big.svg?react';
import './MessageBox.scss';

interface MessageBoxProps extends GeneralMessage {
  onClose: (id: string) => void;
}

const MessageBox: React.FC<MessageBoxProps> = ({
  id,
  text,
  lifeTime,
  onClose,
}) => {
  const messageRef = useRef<HTMLDivElement | null>(null);

  const handleCloseMessage = useCallback(() => {
    messageRef.current?.classList.add('hide');
    setTimeout(() => onClose(id), 600);
  }, [id, onClose]);

  useEffect(() => {
    const timer = setTimeout(() => handleCloseMessage(), lifeTime);

    return () => clearTimeout(timer);
  }, [lifeTime, handleCloseMessage]);

  return (
    <div ref={messageRef} className="message-box">
      <p className="message-box__text">{text}</p>
      <CloseIconBig
        className="message-box__icon"
        onClick={handleCloseMessage}
      />
    </div>
  );
};

export default MessageBox;

 

Using createPortal for the MessageContainer component provides greater control over the rendering location and separation of concerns, ensuring that messages are displayed consistently and avoiding potential CSS conflicts.

/* src/components/MessageContainer/index.tsx */

import React from 'react';
import ReactDom from 'react-dom';
import MessageBox from '@components/MessageBox';
import { GeneralMessage } from '@shared/types/interfaces';
import './MessageContainer.scss';

interface MessageContainerProps {
  messages: GeneralMessage[];
  handleRemoveMessage: (id: string) => void;
}

const MessageContainer: React.FC<MessageContainerProps> = ({
  messages,
  handleRemoveMessage,
}) =>
  ReactDom.createPortal(
    <div className="message-container">
      {messages.map((message) => (
        <MessageBox
          key={message.id}
          id={message.id}
          text={message.text}
          lifeTime={message.lifeTime}
          onClose={handleRemoveMessage}
        />
      ))}
    </div>,
    document.getElementById('portal')!
  );

export default MessageContainer;

 

Integrate MessageContainer into our main app component

/* src/App.tsx */

import React, { useState, useLayoutEffect } from 'react';
//...

const App: React.FC = () => {
  const [messages, setMessages] = useState<GeneralMessage[]>([]);
  /* ... */
  const addNewMessage = (newMessage: Omit<GeneralMessage, 'id'>) => {
    const newMessageArray = [
      ...messages,
      {
        id: uuidv4(),
        ...newMessage,
      },
    ];

    setMessages(newMessageArray);
  };

  const handleRemoveMessage = (messageId: string) => {
    setMessages((prevState) =>
      prevState.filter((message) => message.id !== messageId)
    );
  };

  return (
    <main>
      /* ... */
      <MessageContainer
        messages={messages}
        handleRemoveMessage={handleRemoveMessage}
      />
    </main>
  );
};

export default App;

 

 

Adding smooth transitions to the components

Framer Motion is a powerful tool for enhancing the interactivity and appeal of your app.

The Header component is typically the first thing users see in your app, making it an excellent candidate for some eye-catching motion animation. In the provided code, you can see how motion animation is applied to the Header component using framer-motion.

We're using a motion component to wrap the content of the header component.

/* src/components/Header/index.tsx */

import React from 'react';
import { motion as m } from 'framer-motion';
import { SectionAppearAnimation } from '@shared/animation';
//…

const Header: React.FC<HeaderProps> = ({
  activeThemeMode,
  toggleThemeMode,
}) => (
  <div>
    <m.header
      initial={SectionAppearAnimation.initial}
      animate={SectionAppearAnimation.animate}
      transition={SectionAppearAnimation.transition(0)}
      className="header"
    >
    /* ... */
    </m.header>
  </div>
);

export default Header;

 

Now, let's take a closer look at the SectionAppearAnimation. This animation is designed to make sections of your app appear in a delightful and playful manner. Here's how it works

/* src/shared/animation.ts */

export const SectionAppearAnimation = {
  initial: { scale: 0 },
  animate: { scale: [0, 1.05, 1] },
  transition: (delay = 0) => ({
    delay,
    times: [0, 0.75, 1],
    duration: 1,
    ease: 'easeInOut',
  }),
};

 

Also, we animate various elements within our component using the motion component. In this example, we've animated the color settings container based on the isChangeSettingWidth condition.

/* src/components/GradientGenerator/GradientActivePalette/index.tsx */

import React, { useEffect, useState, useRef } from 'react';
import { motion as m, AnimatePresence } from 'framer-motion';
import useWindowSize from '@shared/hooks/useWindowSize';
//…

const GradientActivePalette: React.FC<GradientActivePaletteProps> = ({
  /* ... */
}) => {
  /* ... */
return (
  const { width } = useWindowSize();
  const isChangeSettingWidth =
    width > parseInt(smallBreakPoint, 10) && !canDeletePalette;
  /* ... */
        <m.div
          initial={isChangeSettingWidth ? { width: 327 } : { width: 'auto' }}
          animate={isChangeSettingWidth ? { width: 383 } : { width: 'auto' }}
          transition={{
            duration: 0.7,
            ease: 'easeInOut',
          }}
          className="gradient-active-color__settings"
        >
  /* ... */

 

We use the same approach for other components, such as GradientTypeAndAngle, GradienPreview, GradientCode and others to create a consistent and engaging user experience throughout our app.

To create animations when elements are removed from the React tree, we'll use the AnimatePresence component. This is particularly useful when you have conditional rendering of elements. In this case, we've wrapped the delete button within AnimatePresence. When the button is removed from the tree it will animate out using the specified animation properties defined in initial, animate, exit, and transition.

/* src/components/GradientGenerator/GradientActivePalette/index.tsx */

import React, { useEffect, useState, useRef } from 'react';
import { motion as m, AnimatePresence } from 'framer-motion';
//…

const GradientActivePalette: React.FC<GradientActivePaletteProps> = ({
  /* ... */
}) => {
  /* ... */
  return (
    <section className="gradient-active-color">
      <h2 className="gradient-generator__subheader">Color</h2>
      <div className="gradient-active-color__content">
      /* ... */
        <AnimatePresence>
          {canDeletePalette && (
            <m.button
              initial={{ scale: 0 }}
              animate={{ scale: 1, rotate: [0, 360] }}
              exit={{ scale: 0, rotate: [0, 360] }}
              transition={{
                duration: 0.7,
                ease: 'easeInOut',
              }}
              type="button"
              aria-label="delete"
              className="gradient-active-color__delete-btn"
              disabled={!canDeletePalette}
              onClick={() => handleDeletePalette(activePalette.id)}
            >
              <TrashIcon className="gradient-active-color__delete-icon" />
            </m.button>
          )}
        </AnimatePresence>
      </div>
    </section>
  );
};

export default GradientActivePalette;

 

Summary

In this comprehensive tutorial, we embarked on a journey to create a powerful tool for generating linear and radial gradients using React. 

Our development process involved several key steps, including setting up a basic layout, implementing a theme mode switcher, generating gradients seamlessly, enabling users to copy gradient CSS code to their clipboard, creating a user-friendly modal for displaying messages, and adding smooth animations to our components.

To accomplish all of this, we leveraged the capabilities of React, harnessed the color manipulation features of the react-colorful library, ensured type safety with TypeScript, and stylized our application using Sass.You can check out the final result here. And don't forget to visit our project's page on GitHub.