⚛️React.js + 🌈Gradients = Gradientti
A guide on how to make a gradient-changing background website.
As a newbie front-end developer, you might be looking for a portfolio project to showcase your front-end skills. Gradientti is a simple app which does this - you can view CSS color gradients, make your own and also copy the generated CSS code.
Details about the App: Live Link | Github Link
The Gradientti app is built using React.js
and Tailwindcss
to make development faster and easier.
Note: this guide only focuses on the core logic/features of the app and leaves the setup and CSS styles out (as there are many articles on the dev stack).
1. The App Structure
The App is divided into four main components namely:
The Header: The
Header
component consists of the App Logo, a color panel displaying the gradient colors, a button to copy the CSS gradient code, a button to add new gradients and a button to view all gradients.The Background: The
Background
which is the main body of the app has the CSS gradient background and button controls to cycle forward or backwards through the list of available gradients.The Sidebar: The
Sidebar
's visibility is controlled by a button. It is like a select menu to view all the gradient swatches and for quick navigation.The Footer: The
Footer
which is optional but nice to have.
2. The useSequence
Hook
The useSequence
hook is the engine behind changing the CSS background gradients. It provides functions to cycle through the list of gradients backwards / forwards or going to a particular index. It uses React's useReducer
state pattern to organize the logic.
import { useReducer } from 'react'
// reducer actions
const INC = 'INCREMENT'
const DEC = 'DECREMENT'
const GOTO = 'GOTO'
const SYNC = 'SYNC'
// hook to provide cycling logic through the list
function useSequence({ count, direction = 0, start = 0, end = 4 }) {
const defaultCount = count || start
const initialState = { count: defaultCount, defaultCount, direction, start, end }
const [state, dispatch] = useReducer(reducer, initialState)
const increment = useCallback(() => dispatch({ type: INC }), [dispatch])
const decrement = useCallback(() => dispatch({ type: DEC }), [dispatch])
const goto = useCallback(index => dispatch({ type: GOTO, index }), [dispatch])
const sync = useCallback((index, bounds = 'end') => dispatch({ type: SYNC, bounds, index }), [dispatch])
return {
...state,
increment,
decrement,
goto,
sync,
}
}
As part of the reducer state
,
the
start
andend
states define the min and max bounds the reducer will cycle through. Ifstart = 0
andend = 4
, the cycle will be0 -> 1 -> 2 -> 3 -> 4
and repeats.count
represents the current position/index of the cycle.direction
- could be-1
,0
or1
to determine if we are moving backwards, static or forwards.
The below reusable functions will dispatch actions
that will update our state
if necessary,
increment()
will cycle forwards whiledecrement()
will cycle backwardsgoto(index)
will cycle to a particular indexsync(index, bounds: end | start)
will update ourstart
orend
bounds to the particular index. This will be useful when dynamically setting the bounds as in the case of adding a new gradient to our predefined gradient list.
// state reducer
function reducer(state, { type, bounds, index }) {
const { count, start, end } = state
const total = end - start + 1
switch (type) {
case INC:
return {
...state,
direction: 1,
count: (count + 1 + total) % total,
}
case DEC:
return {
...state,
direction: -1,
count: (count - 1 + total) % total,
}
case GOTO:
return { ...state, direction: 0, count: clamp(index, start, end) }
case SYNC:
return {
...state,
[bounds]: index,
}
default:
return state
}
}
The clamp
utility function is an extra check/guard to prevent our sequencer from cycling out of the start
and end
bounds.
const clamp = (num, lower, upper) => (upper ? Math.min(Math.max(num, lower), upper) : Math.min(num, lower))
3. Putting It Together
The list of gradients is stored as an array of objects in javascript. For example,
const gradients = [
{
name: 'Cosmic Tail',
start: '#780206',
end: '#061161',
},
{
name: 'Berry Bloom',
start: '#FBD3E9',
end: '#BB377D',
},
...
]
This will be stored in React's state so that we can iterate through it and update our app's UI whenever changes are made to it.
The last gradient in our list will serve as the end
bounds of our useSequence
hook which generates our current index (count
) in the gradient list. The increment()
and decrement()
functions will be assigned to the onClick
event handler of our navigation buttons.
import gradients from './gradients'
function App() {
...
const [gradientList, setGradientList] = useState(gradients)
const { count, increment, decrement } = useSequence({
end: gradientList.length - 1,
})
const { name, start, end } = gradientList[count]
...
// pseudo code
return (
...
<PrevButton onClick={decrement} />
<NextButton onClick={increment} />
<GradientBackground style={{ backgroundImage: `linear-gradient(to right, ${start}, ${end})` }}>{name}</GradientBackground>
...
)
}
4. Displaying the gradientList
in the Sidebar
In this part, we want a button to toggle open the sidebar which displays our list of available gradients in a grid. Whenever a gradient swatch is selected, our gradient background should update to the selected gradient.
Luckily, we can use the Listbox
and Transition
components from the @headlessui/react package to create an animated sidebar.
Because the Listbox
manages the selection internally, we make it a controlled component by providing it value
and onChange
props so that the selected gradient can be available to our app's state and UI.
import { useCallback } from 'react'
import { Listbox } from '@headlessui/react'
// pseudo code
const { count, goto } = useSequence({
end: gradientList.length - 1,
})
const gradient = gradientList[count]
const handleChange = useCallback(
selected => {
goto(selected)
},
[goto]
)
return (
<ListBox value={count} onChange={handleChange}>
...
<ListBox>
)
We pass the current index (count
) as the value
of the ListBox. Whenever the internal value changes, the handleChange()
callback is called and passed the new value. We utilize the goto()
function from the useSequencer
hook to navigate to the new value which becomes the current index in our gradient list.
We also map through the gradient list and wrap each gradient swatch with the Listbox.Options
component. The index of each child is used as the value
of the component.
import { Listbox } from '@headlessui/react'
// pseudo code from GradientsView.js
<Listbox value={count} onChange={handleChange}>
<Lisbox.Button />
<Listbox.Options>
{gradientList.map(({name,start,end}, i) => {
<Listbox.Option key={name} value={i}>
<GradientSwatch style={{backgroundImage: `linear-gradient(to right, ${start}, ${end})`}}>
{name}
</GradientSwatch>
</Listbox.Option>
})}
</Listbox.Options>
</Listbox.Options>
5. Adding a new Gradient Swatch
A nice feature to have in our gradient background changing app is the ability to add new gradient swatches to the default gradient list. The add button in our Header
component will be responsible for toggling open a modal to display a form which can be used to add new gradients.
To help build this feature, we will use the Dialog
component from @headlessui/react package to create the modal, the hex color input and picker from react-colorful package and the @tailwindcss/forms
plugin to reset the HTML form input styles.
We have our formGradientState
which holds the values for each form input - start
, end
and name
and the errorsState
for validation and submission errors.
import { useState } from 'react'
// initialGradient from the App's State to initialize form inputs
const [newGradient, setNewGradient] = useState(initialGradient)
const [errors, setErrors] = useState({})
const { start, end, name } = newGradient
const { gradient: errGradient, name: errName } = errors
The HexColorPicker
and HexColorInput
from react-colorful
have an internally managed state in which we can access the selected color through their onChange
callback function. The components have in-built validation (prevent empty inputs, incorrect hex colors etc) hence error handling is done only for form submission.
import { HexColorInput, HexColorPicker } from 'react-colorful'
// pseudo code
const color: start | end
// handler for both start and end colors
const handleColorChange = useCallback(
key => color => {
setNewGradient(_gradient => ({ ..._gradient, [key]: color
}))
// clear errors on color input change
setErrors(_errors => ({ ..._errors, gradient: '' }))
},
[]
)
return (
...
<HexColorInput color={color} onChange={handleColorChange('start')} />
<HexColorPicker color={color} onChange={handleColorChange('start')} />
...
{errGradient && <ErrorText text="Gradient already exists."/>
)
In contrast, the form input for the gradient name uses the HTML client-side validation to guard against errors.
import { useCallback } from 'react'
const handleNameChange = useCallback(({ target }) => {
target.setCustomValidity('')
setNewGradient(_gradient => ({ ..._gradient, name:
target.value }))
setErrors(_errors => ({ ..._errors, name: '' }))
}, [])
const handleNameValidity = useCallback(({ target }) => {
if (target.validity.valueMissing) {
target.setCustomValidity('Name is required.')
} else if (target.validity.patternMismatch) {
target.setCustomValidity('Name is invalid. Use 2 or more
letters.')
}
}, [])
return (
...
<input
value={name}
onChange={handleNameChange}
onInvalid={handleNameValidity}
type="text"
pattern="[a-zA-Z]+[\s]?[A-Za-z]+"
required
/>
{errName && <ErrorText text="Name already exists."/>
...
)
The ErrorText
component displays errors from the form submission. When we want to add a new gradient, we first check if that gradient swatch does not exist in our gradient list before submitting.
// utility to check if new gradient is in our gradient list
function checkIfExist(list, item) {
const nameExist = list.some(_item => _item.name === item.name)
const gradientExist = list.some(_item => _item.start ===
item.start && _item.end === item.end)
return { gradientExist, nameExist }
}
// form submission handler
const handleSubmit = useCallback(
e => {
e.preventDefault()
const { gradientExist = '', nameExist = '' } =
checkIfExist(gradientList, newGradient)
setErrors({ name: nameExist, gradient: gradientExist })
if (!(gradientExist || nameExist)) {
onMake?.(newGradient)
}
},
[gradientList, newGradient, onMake]
)
The onMake()
callback sends the new gradient to our App when all validation has been done successfully during submission.
In our App, we use the handleGradientAdd()
callback to retrieve the new gradient and add it to our gradient list. The new gradient is inserted at the index after our currently displayed gradient. We also update our end
bounds to accommodate the additional gradient and then navigate to the new gradient using increment()
.
import { useRef } from 'react'
function App() {
...
const lastGradientIndex = gradientList.length - 1
const lastGradientIndexRef = useRef(lastGradientIndex)
const { count, increment, sync } = useSequence({
end: lastGradientIndex,
})
const handleGradientAdd = useCallback(
newGradient => {
setGradientList(list => {
lastGradientIndexRef.current = list.length
return insertAt(list, count + 1, newGradient)
})
sync(lastGradientIndexRef.current)
increment()
},
[count, increment, sync]
)
...
}
6. Copying the CSS Gradient Code
Another useful feature to have in the app is for anyone to copy the CSS gradient code easily. When we click the copy button in the Header
component, we open the modal with the CSS gradient code block for the currently displayed gradient background.
In the modal, there's a vendor prefix checkbox to toggle browser compatibility code and a button to copy the CSS code. This is easily achieved using the useClipboard
hook from the use-clipboard-copy package and the prismjs package for CSS syntax highlighting.
import { useEffect } from 'react'
import { highlightAll } from 'prismjs/components/prism-core'
import 'prismjs/components/prism-css'
import 'prismjs/plugins/line-numbers/prism-line-numbers'
import 'prismjs/plugins/line-numbers/prism-line-numbers.css'
import 'prismjs/plugins/normalize-whitespace/prism-normalize-whitespace'
function CodeBlock({ code }) {
useEffect(() => {
highlightAll()
}, [code])
return (
<div className="overflow-auto">
<pre className="language-css line-numbers">
<code>{code}</code>
</pre>
</div>
)
}
import { useState, useCallback } from 'react'
import { useClipboard } from 'use-clipboard-copy'
// pseudo code
const code: prefixedCss | normalCss
const [prefix, setPrefix] = useState(true)
const { copy, copied } = useClipboard({
copiedTimeout: 2000,
})
const handleChange = useCallback(e => {
setPrefix(e.target.checked)
}, [])
const handleCopy = useCallback(() => {
copy(code)
}, [copy, code])
return (
...
<Codeblock code={code} />
...
<input type="checkbox" checked={prefix} onChange=
{handleChange}/>
<button onClick={handleCopy}>Copy CSS</button>
...
)
In conclusion, we've highlighted the logic and features found in our gradient-changing background app. The full code is accessible at the GitHub repo. Please don't forget to star the repo if you found it useful.