ProNextJS
    Loading
    lesson

    Refine the Layout with CSS Modules

    Jack HerringtonJack Herrington

    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.

    Card padding

    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.

    3 Columns after the CSS Reset

    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 divs: 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!

    Layout progress

    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:

    Rounded image corners

    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:

    Vertical mode card

    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!

    font styles applied

    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.

    Dark mode enabled

    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.

    Transcript