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:
Hold on to your keyboards, guys!
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;
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;
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;
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.
24.12.2023
Agile vs Waterfall Software Development Methods: 7 Key DifferencesAgile and Waterfall are the two most prominent methods in the software development industry today. Although they are both solid and offer the most practical way to finish a project as quickly as possible, they're different in many ways. Here are 7 of them.Read more22.12.2023
How to Build a Tool for Generating Linear and Radial Gradients with React: Complete Tutorial, Part 2Hello! Welcome back to part two 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.Read more22.12.2023
How to Build a Tool for Generating Linear and Radial Gradients with React: Complete Tutorial, Part 1Hey fellow developers and welcome to an exciting journey into the colorful world of gradient generation! In this tutorial, we'll use React, 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.Read more