Refine the Layout with CSS Modules
Let's start by adding a className
to our card and specifying styles.card
to the div
surrounding the ProductCard
component:
<main className={styles.main}>
{PRODUCTS.map((product) => (
<div key={product.id} className={styles.card}>
<ProductCard {...product} />
</div>
))}
</main>
After saving, we can see that we get a two-column layout with some nice padding, making it look more like a card.
However, we don't have a CSS reset in place, which means there are some default CSS styles that aren't doing us any favors when it comes to box sizing.
Add a CSS Reset
To fix this, let's add a simple reset inside of the globals.css
file. This reset sets the box-sizing
to border-box
and establishes basic defaults for img
elements:
*,
*::before,
*::after {
box-sizing: border-box;
}
img {
max-width: 100%;
display: block;
}
There are larger resets available, but this one will work well for our application.
With the reset in place, we now have a nice three-column layout that's starting to look really good.
Making Cards Responsive with Container Queries
Next, we're going to focus on making our cards inherently responsive using container queries. We'll also give the cards rounded edges for a polished look.
The component that manages the cards is ProductCard
, so we will create a corresponding ProductCard.module.css
file to style it.
Looking at the structure of the ProductCard
, we have a series of nested div
elements. The top-level div
will be our container, specifying that it's the container for the card in a container query system.
export const ProductCard = ({ product }: Props) => {
return (
<div>
<div>
<div>
<Image
src={product.image}
alt={product.title}
width={300}
height={300}
/>
</div>
<div>
<h1>{product.title}</h1>
<p>{product.price}</p>
</div>
</div>
</div>
);
};
Container queries are a powerful feature that allows you to create layouts dependent on the size of the container, similar to media queries, but instead of being based on the viewport size, they're based on the specified container.
Inside of the the new ProductCard.module.css
file, we'll establish the card
class and set the container-type
to inline-size
:
.card {
container-type: inline-size;
}
Back in the component, we'll import the styles from ProductCard.module.css
and apply the card
class to the top-level div
:
// inside ProductCard.tsx
import styles from './ProductCard.module.css';
export const ProductCard = ({ product }: Props) => {
return (
<div className={styles.card}>
<div>
...
At this point, we won't see any visible difference because we're just establishing the card as a container. In order to have visible changes there's more work to do.
Using Flexbox for Layout
Inside the ProductCard
, we have a nested div
system. The top-level div
contains two nested div
s: the image
div and the info
div, which holds the title and price.
We're going to use Flexbox to control the layout. In the vertical mode, we'll set it to a flex-column
, and in the horizontal mode where the image is on the left and the info is on the right, we'll set it to a flex-row
, which is the default.
Let's create a cardContainer
class, which will always use flex
:
/* inside ProductCard.module.css */
.cardContainer {
display: flex;
}
In the vertical layout mode (when the container width is less than or equal to 600px), we'll set the flex-direction
to column
. For the horizontal layout, we'll keep the default row
format so its container query will remain empty for now:
/* inside ProductCard.module.css */
/* Vertical layout */
@container (max-width: 450px) {
.cardContainer {
flex-direction: column;
}
}
/* Horizontal layout */
@container (min-width: 450px) {
}
Back in the component, we'll apply the cardContainer
class to the outer div
:
export const ProductCard = ({ product }: Props) => {
return (
<div className={styles.card}>
<div className={styles.cardContainer}>
<div>
<Image
src={product.image}
alt={product.title}
width={300}
height={300}
/>
</div>
...
After saving, we can see that in small sizes, we have a horizontal layout, and in large sizes, we have a vertical layout. It's looking good!
Next, we'll fix the image and make it responsive.
Creating a Responsive Image
Back in the CSS file, we need to create an imageContainer
class for the nested div
around the img
tag.
We'll start by specifying that the img
inside the imageContainer
is responsive by setting the width
to 100%
and the height
to auto
.
In the vertical layout, the imageContainer
will take up 100%
width and have rounded corners on the top-left and top-right.
In the horizontal layout, the imageContainer
will take up 25%
of the horizontal space, and the img
will have rounded corners on the top-left and bottom-left:
/* inside ProductCard.module.css */
/* ...other classes as before... */
.imageContainer img {
width: 100%;
height: auto;
}
/* Vertical layout */
@container (max-width: 450px) {
.cardContainer {
flex-direction: column;
}
.imageContainer {
width: 100%;
}
.imageContainer img {
border-top-right-radius: 1rem;
border-top-left-radius: 1rem;
}
}
/* Horizontal layout */
@container (min-width: 450px) {
.imageContainer {
width: 25%;
}
.imageContainer img {
border-top-left-radius: 1rem;
border-bottom-left-radius: 1rem;
}
}
Let's attach the imageContainer
class to the div
around the img
:
export const ProductCard = ({ product }: Props) => {
return (
<div className={styles.card}>
<div className={styles.cardContainer}>
<div className={styles.imageContainer}>
<Image
src={product.image}
alt={product.title}
width={300}
height={300}
/>
</div>
...
After saving, the image looks great with the rounded corners in both layouts:
However, the additional information could look better.
Styling the Info Container
The title and price are contained within a div
that we'll call the infoContainer
. It will add some padding to separate it from the image and give us a nice rounded border to define the card.
Let's create the infoContainer
class, which will have some padding-left
to create space between the image and the info:
.infoContainer {
padding-left: 16px;
}
In the vertical layout, the infoContainer
will take up 100%
width and have rounded corners on the bottom-left and bottom-right.
In the horizontal layout, the infoContainer
will take up 75%
width (since the image is 25%), and it will have rounded corners on the top-right, bottom-right, and bottom-left:
/* Vertical layout */
@container (max-width: 450px) {
.cardContainer {
flex-direction: column;
}
.imageContainer {
width: 100%;
}
.imageContainer img {
border-top-right-radius: 1rem;
border-top-left-radius: 1rem;
}
.infoContainer {
width: 100%;
border-bottom: 1px solid #666;
border-top: none;
border-left: 1px solid #666;
border-right: 1px solid #666;
border-bottom-right-radius: 1rem;
border-bottom-left-radius: 1rem;
}
}
/* Horizontal layout */
@container (min-width: 450px) {
.imageContainer {
width: 25%;
}
.imageContainer img {
border-top-left-radius: 1rem;
border-bottom-left-radius: 1rem;
}
.infoContainer {
width: 75%;
border-bottom: 1px solid #666;
border-top: 1px solid #666;
border-right: 1px solid #666;
border-top-right-radius: 1rem;
border-bottom-right-radius: 1rem;
}
}
We can now attach the infoContainer
class to the div
around the title and price:
export const ProductCard = ({ product }: Props) => {
return (
<div className={styles.card}>
<div className={styles.cardContainer}>
<div className={styles.imageContainer}>
<Image
src={product.image}
alt={product.title}
width={300}
height={300}
/>
</div>
<div className={styles.infoContainer}>
<h1>{product.title}</h3>
<p>${product.price}</p>
</div>
...
Now, the card is looking great with the rounded borders around the info section:
Styling the Title and Price
With the card layout looking good, let's clean up the formatting of the title and price by creating some classes for them. We'll add some font sizes and margins to make them look nicer:
.title {
font-size: 1.5rem;
margin: 1rem 0 0 0;
}
.price {
font-size: 1rem;
font-style: italic;
margin: 0 0 1rem 0;
}
Let's attach these classes to the respective elements:
// inside the infoContainer div
<h1 className={styles.title}>{title}</h3>
<p className={styles.price}>${price}</p>
With that, our card styling is complete!
Supporting Dark Mode
The only thing left is to handle dark and light mode, which is a really easy fix.
In globals.css
, we can specify the default light mode styles:
body {
background-color: white;
color: black;
}
Then, we can add a media query to flip the colors if the user prefers the dark color scheme:
@media (prefers-color-scheme: dark) {
body {
background-color: black;
color: white;
}
}
Now, as we toggle between light and dark mode, we can see the card adapting accordingly.
That's it! Using CSS modules makes styling a breeze. You create classes, import them, and it automatically handles the hashing of the class names. It's a predictable and reliable way to style your applications.
By walking through the CSS step-by-step, hopefully you now understand not only how we used CSS modules but also how the application is laid out, the value of media queries and container queries, and how to implement light and dark mode.
Please refer back to this video if you get confused about how we're attaching classes in other videos. The CSS itself will be exactly the same; we'll just be using different mechanisms for specifying that CSS.