In the old days, when we wanted a hero image that would fill an entire responsive div element, we would use the background-image, background-size, etc...
CSS tags to place our hero image behind the hero foreground. And for years this worked great!
That is... until next-image
came along. NextJS is a JavaScript framework on top of a framework (React). It provides additional abstraction and functionality on top of React, including a built-in express server with filesystem routes, static generated pages and automatic responsive image optimization when you host your app on Vercel (which is he creator of NextJS). The image optimization is great. It automagically generates srcset
images and converts them to webp
.
So that's all well and good, but here's the problem: next-image does not work with CSS-defined background images. 😱 But no worries! By using next-image in conjunction with a little Tailwind CSS, we can create a modern hero component that is fully responsive, image-optimized and relatively future-proof... well, at least for the next week or two. 🤣
This tutorial focuses on creating the hero component and is part of a larger series on how to build a complete website using NextJS 13 (yet to be published). You can clone this project as it currently stands here. That being said, I will not be covering the basics of how to create a new NextJS 13 app.
Installing Tailwind CSS
If you don't already have Tailwind CSS in your app, let's get it installed:
npm install -D tailwindcss postcss autoprefixer
npm install react-cool-dimensions
npx tailwindcss init
This installs our dependencies and initializes our config files. It also installs react-cool-dimensions
, which we will use for shipping properly sized images to the client.
Next, we need to edit a couple of files, post.config.css
and tailwind.config.css
:
// postcss.config.js
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
}
}
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
content: [
"./src/pages/**/*.{js,ts,jsx,tsx}",
"./src/components/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {
colors: {
'primary': '#29abe3',
'primaryDark': '#3829e3',
'logo': '#edebeb',
'bg1': '#4c05b0',
},
},
},
plugins: [],
}
Not that I created some custom utility classes for our theme colors.
Now we want to add the following to the top of our global CSS file, which is usually in ./styles/globals.css
:
@tailwind base;
@tailwind components;
@tailwind utilities;
Create a Button Component
We first want to create our child components before we create our higher order component. The only component our hero will have is a button. This example is a nice universal button component that will render either an <a>
or <button>
element depending on whether or not the href
property is supplied:
import React from 'react'
import Link from 'next/link';
export default function Button(props, { variant = 'primary', children }) {
let className = `mt-8 px-12 py-3 bg-gradient-to-r from-primary to-primaryDark`
+ ' hover:opacity-90 text-xl text-white/90 font-semibold drop-shadow-lg rounded-full';
return props.href
? <Link className={className} {...props} />
: <button type="button" className={className} {...props} />
}
Create Our Hero Component
Now we can create our hero component. Don't be alarmed by the complexity, I will explain in full:
import React, { useState } from 'react'
import Image from 'next/image'
import useDimensions from 'react-cool-dimensions';
import arrayCeil from '../lib/arrayCeil';
import Button from './ui/Button';
export default function Hero() {
const [heroImage, setHeroImage] = useState('hero-1920.jpg')
const imageSizes = [600, 1280, 1920]
const { observe, unobserve, width, height, entry } = useDimensions({
onResize: ({ observe, unobserve, width, height, entry }) => {
setHeroImage(`hero-${arrayCeil(imageSizes, width)}.jpg`)
unobserve(); // To stop observing the current target element
observe(); // To re-start observing the current target element
},
});
return (
<div
ref={observe}
class="w-full h-screen flex justify-center items-center overflow-hidden relative bg-black">
<Image
src={`/images/${heroImage}`}
alt="Hero Image"
className="opacity-60 object-cover"
fill
/>
<div class="flex flex-col justify-center items-center px-3">
<h1 class=" text-center text-3xl md:text-5xl text-white font-bold drop-shadow-lg">WELCOME TO <br />
<span class="text-primary">MY COMPANY</span>
</h1>
<p class="mt-5 text-center text-lg text-white opacity-90">Making tomorrows widgets today...</p>
<Button href="/">Get Started</Button>
</div>
</div>
)
}
So what we have here is an images folder with images we created for our hero based on the viewport's max width, which we detect using the useDimensions()
hook. Every time the viewport width is changed, this hook fires and rounds the new dimension up to the next highest value in our array of dimensions using the custom arrayCeil()
function. This allows us to serve properly sized and cropped images for our hero.
Here is the code for arrayCeil()
:
export default function arrayCeil(arr, number) {
const sorted = arr.sort((a, b) => a - b);
for (let i = 0; i < arr.length; i++) {
if (arr[i] > number) {
return sorted[i];
}
}
// If no index found return the last element
return sorted[sorted.length - 1];
}
Pretty simple, we just loop through the array until we find the index of the next highest element compared to the supplied value, and then return the value of that index. This should be built in to JavaScript! 😡
Now npm run dev
your app and hopefully you see this:
And here's the mobile phone view:
Well, that's all for this tutorial. Be sure to check back soon for my upcoming tutorial: a complete NextJS 13 website! You'll be able to find that and much more at the Designly Blog.