DEV Community

Cover image for Getting started with NextUI and Next.js
Matt Angelosanto for LogRocket

Posted on • Originally published at blog.logrocket.com

Getting started with NextUI and Next.js

Written by Miracle Onyenma✏️

Getting your projects from the idea phase to development and then production as soon as possible while keeping them functional and beautiful can be daunting, especially when it doesn’t involve a dedicated team of developers.

Fortunately, UI libraries provide developers with pre-built sets of functions and components that can be added to and modified in an application. This can be very useful.

In this article, we will look at a new UI library for React and Next.js, NextUI. Like Material-UI (MUI), a popular UI or component library, NextUI provides many reusable components in many applications, such as a navbar, tooltip, tab, pagination components, and so on.

We’ll explore how we can use this UI library to build a simple ecommerce application. To follow along, you should have a basic understanding of JavaScript, React, and Next.js. You will also need Node.js LTS v18.13 and VS Code installed on your machine.

Jump ahead:

What is a UI library?

A UI library, in most cases, is simply a collection of pre-built UI components and elements that can be integrated into a project and customized to some degree to facilitate product development.

These UI components and elements usually include Button, Navbar, Tooltip, Tab components, and more. Many UI libraries exist, including React Bootstrap, built on the popular Bootstrap CSS library, and Material-UI, one of the most popular UI component libraries for React.

What is NextUI?

According to the docs, NextUI is a React UI library that allows you to make beautiful, modern, and fast websites/applications regardless of your design experience. It is created with React and Stitches, based on React Aria, and inspired by Vuesax.

Why NextUI?

NextUI is a great choice for a few different reasons. NextUI is a beautifully designed UI framework with visually appealing component style defaults. NextUI also has accessible components and follows the WAI-ARIA standards offering keyboard support and logical focus management.

NextUI is built on top of the CSS-in-JS library Stitches. You can customize any component in several ways using the css prop, styled function, or native CSS selectors.

NextUI also provides a simple way to customize the default themes, change the colors, fonts, breakpoints, and more. NextUI also has a dark theme that you can apply to your application with just a few lines of code.

Setting up Next.js and NextUI

We’ll explore how we can use UI components from the NextUI UI library to build a simple ecommerce site. Let’s follow the steps to build a Next.js app. You can find the full code here and preview the final project here. Let's get into it.

First, navigate to the folder of choice and run the command:

npx create-next-app@latest
# or
yarn create next-app
# or
pnpm create next-app
Enter fullscreen mode Exit fullscreen mode

Once installed, navigate to the newly created directory to install the NextUI package by running either of the following:

yarn add @nextui-org/react
# or
npm i @nextui-org/react
Enter fullscreen mode Exit fullscreen mode

For NextUI to work correctly, we need to set up the NextUIProvider at the root of our application. We’ll go to pages/_app.js and add the following:

// ./pages/_app.js

// import `NextUIProvider` component
import { NextUIProvider } from "@nextui-org/react";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
  return (
    // Use at the root of our app
    <NextUIProvider>
      <Component {...pageProps} />
    </NextUIProvider>
  );
}
export default MyApp;

Next, we'll create a new file ./pages/_document.js and add the following:

// ./pages/_document.js

import Document, { Html, Head, Main, NextScript } from 'next/document';
import { CssBaseline } from '@nextui-org/react';

class MyDocument extends Document {
  static async getInitialProps(ctx) {
    const initialProps = await Document.getInitialProps(ctx);
    return {
      ...initialProps,
      styles: <>{initialProps.styles}</>
    };
  }

  render() {
    return (
      <Html>
        <Head>{CssBaseline.flush()}</Head>
        <body>
          <Main />
          <NextScript />
        </body>
      </Html>
    );
  }
}

export default MyDocument;
Enter fullscreen mode Exit fullscreen mode

Now that NextUI is installed and set up, we can use any available components and build our application.

Building out our application

Now, we’ll build out our SiteHeader using the NextUI Navbar component. First, create a new file named ./components/SiteHeader.jsx:

// ./components/SiteHeader.jsx
import { Avatar, Button, Dropdown, Navbar, Text } from "@nextui-org/react";
import { ShoppingCartIcon, UserIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
const SiteHeader = () => {
  return (
    <Navbar variant="floating" isBordered>
      <Navbar.Brand>
        <Link href="/">
          <Text weight="bold">Shoppping</Text>
        </Link>
      </Navbar.Brand>
      <Navbar.Content>
        <Navbar.Link>
          <Button as={Link} href="/cart" auto css={{ padding: "$4" }} light>
            <ShoppingCartIcon width={24} />
          </Button>
        </Navbar.Link>
        <Navbar.Item>
          <Dropdown>
            <Dropdown.Trigger>
              <Avatar squared icon={<UserIcon width={24} />} />
            </Dropdown.Trigger>
            <Dropdown.Menu>
              <Dropdown.Item key="Profile">Profile</Dropdown.Item>
              <Dropdown.Item key="Orders">Orders</Dropdown.Item>
              <Dropdown.Section>
                <Dropdown.Item key="Settings">Settings</Dropdown.Item>
                <Dropdown.Item key="Logout" color="error">
                  Logout
                </Dropdown.Item>
              </Dropdown.Section>
            </Dropdown.Menu>
          </Dropdown>
        </Navbar.Item>
      </Navbar.Content>
    </Navbar>
  );
};
export default SiteHeader;
Enter fullscreen mode Exit fullscreen mode

Here, we import the NextUI Navbar component that contains all our other Avatar, Button, Dropdown, and Text components.

If you look closely, you’ll notice that the main Navbar component consists of the following sub-components:

  • Navbar.Brand: A simple and flexible wrapper for branding content
  • Navbar.Content: A wrapper that provides the state and variants for the navbar content items
  • Navbar.Item: Individual items that must be a direct child of Navbar.Content
  • Navbar.Link: A link item that must be a direct child of Navbar.Content

You can view more on the anatomy of the Navbar component here.

For the cart button, NextUI allows us to change which tag component outputs. We use a Button component with the as attribute of Link that allows us to output the Next.js Link component.

To achieve the user account dropdown menu, we used the Dropdown component consisting of the following sub-components:

  • Dropdown.Trigger: Used to wrap the reference (or trigger) element. Similar to Dropdown.Button but without the chevron icon
  • Dropdown.Menu: The wrapper for the items. Must be a direct child of Dropdown
  • Dropdown.Section: A wrapper to group-related items
  • Dropdown.Item: The individual items. Must be a direct child of Dropdown.Menu

You can view more on the anatomy of the Button here.

Here’s what our SiteHeader should look like now: NextUI and Next.js Site Header Example

Adding a store-hero to the homepage

Let’s add a store-hero/banner to the homepage, introducing us to the Container, Card, and Spacer components. Back in our ./pages/index.js page, add the following:

// ./pages/index.js

import Head from "next/head";
import {
  Card,
  Container,
  Spacer,
  Text,
} from "@nextui-org/react";

export default function Home() {
  return (
    <>
      <Head>
        <title>Shopping app</title>
      </Head>
      <main>
        <header className="store-hero">
          <Spacer y={2} />
          <Container md>
            <Card variant="bordered">
              <Card.Body css={{ padding: "$24" }}>
                <Text h1 css={{ textGradient: "45deg, $blue600 -20%, $pink600 50%" }}>
                  Welcome to NextUI + Next.js Shopping app
                </Text>
                <Text size={24}>
                  This is a simple shopping app built with NextUI and Next.js.
                  This is a demo app to showcase NextUI components.
                </Text>
              </Card.Body>
            </Card>
          </Container>
        </header>
      </main>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Here, we use a Spacer with a y (vertical) value of 2 to give space between the SiteHeader and store-hero.

Next, we set an md breakpoint to the Container. This ensures that the container will be fluid. For example, have a width of 100% at all breakpoints (<650px and >650px) until 960px, where the width will now be fixed at 960px.

We also introduced the Card, which contains our site-hero content. The Card also consists of a few sub-components, but we’re using only Card.Body, which contains the contents of the card. You can explore the anatomy of the Card here.

We also customize Card.Body and Text styling by using the css prop. For the Card, we set the padding using a spacing token of $24.

Here’s what we should end up with: NextUI and Next.js Application Start Next, we’ll fetch product data within getServerSideProps from a demo API — https://dummyjson.com/docs/products.

Fetching product data

In ./pages/index.js, we’ll use the Fetch API to fetch data within getServerSideProps and return the data as props:

// ./pages/index.js

// ...

export const getServerSideProps = async () => {
  try {
    const res = await fetch("https://dummyjson.com/products?limit=8");
    const { products } = await res.json();
    return {
      props: { products },
    };
  } catch (error) {
    console.error(error);
    return {
      props: { products: [] },
    };
  }
};

export default function Home({ products }) {
  return (
    <>
      {/* ... */}
      <main>
        {/* ... */}
        <ul>
          {products.map((product) => (
            <li key={product.id}>
              <h2>{product.title}</h2>
              <p>{product.description}</p>
              <p>{product.price}</p>
            </li>
          ))}
        </ul>
      </main>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

Within the Home component, we render a list of products with titles, descriptions, and prices: Cart Items in the NextUI and Next.js App Next, we’re going to improve the UI by using the Cards component and building out a grid layout with Grid to display our products.

Creating the ProductCard component

First, create a new file called ./components/ProductCard.jsx:

// ./components/ProductCard.jsx

import { Button, Card, Col, Row, Spacer, Text } from "@nextui-org/react";
import Link from "next/link";

const ProductCard = ({ product }) => {

  const { id, title, description, price, images } = product;

  return (
    <Card isPressable variant="bordered" css={{ w: "100%", h: "500px" }} as={Link} href={`products/${id}`}>
      <Card.Body css={{ p: 0 }}>
        <Card.Image
          src={images[0]}
          objectFit="cover"
          width="100%"
          height="100%"
          alt={title}
        />
      </Card.Body>
      <Card.Footer
        isBlurred
        css={{
          position: "absolute",
          p: "$8",
          bgBlur: "#ffffffcc",
          borderTop: "$borderWeights$light solid rgba(255, 255, 255, 0.2)",
          bottom: 0,
          zIndex: 1,
        }}
      >
        <Row>
          <Col>
            <Row justify="space-between" align="top">
              <Col>
                <Text h3>{title}</Text>
                <Text
                  size={"$sm"}
                >
                  {description}
                </Text>
              </Col>
              <Col css={{ width: "auto" }}>
                <Text
                  css={{
                    color: "$accents7",
                    fontWeight: "$semibold",
                    fontSize: "$2xl",
                    pl: "$12",
                  }}
                >
                  ${price}
                </Text>
              </Col>
            </Row>
            <Spacer y={1} />
            <Button css={{ w: "100%" }}>Add to cart</Button>
          </Col>
        </Row>
      </Card.Footer>
    </Card>
  );
};
export default ProductCard;
Enter fullscreen mode Exit fullscreen mode

Here, we use additional Card sub-components, Card.Image with some custom CSS to display the product image and Card.Footer. This contains Row and Col components, which will contain the product title, description, and price: Building Out the NextUI and Next.js app Next, we’ll build out the add-to-cart functionality, and to do that, we have to quickly set up global state management in Next.js using Context API.

Global state management using Context API

Context API is a state management tool bundled with the React library itself. After setting up Context in our application, we will be able to access chosen state throughout our application.

To set up, first, create a new file called ./modules/AppContext.js:

// ./modules/AppContext.js
const { createContext, useState, useContext, useReducer } = require("react");
// context data getter
const CartStateContext = createContext();
// context data setter
const CartDispatchContext = createContext();
// get item from cart
const getItem = (cart, product) => {
  return cart.find((item) => item.id === product.id);
};
const cartReducer = (state, action) => {
  // get the product object and the type of action by destructuring
  const { product, type } = action;
  const item = getItem(state, product);
  // if "add"
  // return an array of the previous state and the product object
  if (type === "add") {
    return item
      ? state.map((cartItem) =>
          cartItem.id === product.id
            ? { ...cartItem, quantity: cartItem.quantity + 1 }
            : cartItem
        )
      : [...state, { ...product, quantity: 1 }];
  }
  // if "remove"
  // remove the product object in the previous state
  // that matches the title of the current product object
  if (type === "remove") {
    return item.quantity === 1
      ? state.filter((cartItem) => cartItem.id !== product.id)
      : state.map((cartItem) =>
          cartItem.id === product.id
            ? { ...cartItem, quantity: cartItem.quantity - 1 }
            : cartItem
        );
  }
  // if "delete"
  // remove the product object from the state
  if (type === "delete") {
    return state.filter((cartItem) => cartItem.id !== product.id);
  }
  return state;
};
export const CartProvider = ({ children }) => {
  const [cart, setCart] = useReducer(cartReducer, []);
  return (
    <CartDispatchContext.Provider value={setCart}>
      <CartStateContext.Provider value={cart}>
        {children}
      </CartStateContext.Provider>
    </CartDispatchContext.Provider>
  );
};
export const useDispatchCart = () => useContext(CartDispatchContext);
export const useCart = () => useContext(CartStateContext);
Enter fullscreen mode Exit fullscreen mode

Here, we have CartStateContext and CartDispatchContext, which act as the data getters and setters, respectively. The cartReducer function adds, removes, or deletes state depending on the defined type.

Within the CartProvider function, we have: const [cart, setCart] = useReducer(cartReducer, []);.

Here, using the useReducer Hook, cart gets the data, and setCart updates the data using the cartReducer function defined earlier.

We also return the DispatchContext and StateContext providers, which wraps the children prop. And, at the end of the file, the context is exported using the useContext Hook.

For the entire application to have access to the context, we need to include it in our ./pages/app.js file:

// ./pages/_app.js
// import `NextUIProvider` component
import { NextUIProvider } from "@nextui-org/react";
import DefaultLayout from "../layouts/Default";
import { CartProvider } from "../modules/AppContext";
import "../styles/globals.css";
function MyApp({ Component, pageProps }) {
  return (
    // Use at the root of our app
    <NextUIProvider>
      <CartProvider>
        <DefaultLayout>
          <Component {...pageProps} />
        </DefaultLayout>
      </CartProvider>
    </NextUIProvider>
  );
}
export default MyApp;
Enter fullscreen mode Exit fullscreen mode

Great! Now, let's add an add-to-cart function in our productcard component:

// ./components/ProductCard.jsx

import { useState, useEffect } from "react";
import { Button, Card, Col, Row, Spacer, Text } from "@nextui-org/react";
import Link from "next/link";
import { useCart, useDispatchCart } from "../modules/AppContext";
const ProductCard = ({ product }) => {

  const { id, title, description, price, images } = product;

  const cart = useCart();
  const setCart = useDispatchCart();
  const [isAdded, setIsAdded] = useState(false);
  const handleAddToCart = (product) => {
    setCart({ type: "add", product });
    setIsAdded(true);
  };

  const checkIfAdded = (product) => {
    const found = cart.find((item) => item.id === product.id);
    if (found) {
      setIsAdded(true);
    } else {
      setIsAdded(false);
    }
  };
  useEffect(() => {
    checkIfAdded(product);
  }, []);

  return (
    <Card isPressable variant="bordered" css={{ w: "100%", h: "500px" }}>
      <Card.Body css={{ p: 0 }}>
        {/* ... */}
      </Card.Body>
      <Card.Footer>
        <Row>
          <Col>
             {/* ... */}
            <Button
              disabled={isAdded}
              onClick={() => handleAddToCart(product)}
              css={{ w: "100%" }}
            >
              {isAdded ? "Added to cart" : "Add to cart"}
            </Button>
          </Col>
        </Row>
      </Card.Footer>
    </Card>
  );
};

export default ProductCard;
Enter fullscreen mode Exit fullscreen mode

Here, we import useCart and useDispatchCart from our app context. We then initialize it within the ProductCard function.

The handleAddToCart function, triggered by the add to cart button, adds the selected product to the cart and the checkIfAdded function. This is triggered by useEffect(), which runs when the component is loaded and then sets the isAdded state to true or false, depending on if the product is found in the cart state.

Now, that the state is global, let’s explore another useful NextUI component, Badges.

Adding the Badge to display the number of cart items

To do this, we go to our SiteHeader component ./components/SiteHeader.jsx:

// ./components/SiteHeader.jsx
import {
  Avatar,
  Badge,
  Button,
  Dropdown,
  Navbar,
  Text,
} from "@nextui-org/react";
import { ShoppingCartIcon, UserIcon } from "@heroicons/react/24/solid";
import Link from "next/link";
import { useCart } from "../modules/AppContext";
const SiteHeader = () => {
  const cart = useCart();
  return (
    <Navbar variant="floating" isBordered>
      {/* ... */}
      <Navbar.Content>
        <Navbar.Link>
          <Badge color={"primary"} content={cart.length} variant="flat">
            <Button as={Link} href="/cart" auto css={{ padding: "$4" }} light>
              <ShoppingCartIcon width={24} />
            </Button>
          </Badge>
        </Navbar.Link>
        {/* ... */}
      </Navbar.Content>
    </Navbar>
  );
};
export default SiteHeader;
Enter fullscreen mode Exit fullscreen mode

Here, within our SiteHeader, we wrap the shopping cart button with the Badge. This allows the badge content to be positioned relative to the children, in this case, the button.

Then, we pass cart.length to the content prop of the Badge, which is displayed as seen in the image below: Adding to the NextUI and Next.js app

Next, we’ll build out the cart page. In order to do that, we’ll create a CartItem component.

Creating the CartItem component

Now, create a new file called ./components/CartItem.jsx:

// ./components/CartItem.jsx
import { Button, Card, Col, Grid, Row, Text } from "@nextui-org/react";
import { TrashIcon } from "@heroicons/react/24/solid";
import { useDispatchCart } from "../modules/AppContext";
const CartItem = ({ item }) => {
  const setCart = useDispatchCart();
  return (
    <Grid xs={12}>
      <Card variant={"bordered"}>
        <Card.Body css={{ p: "$10" }}>
          <Row css={{ gap: "$12" }}>
            <Col css={{ width: "200px", height: "200px" }}>
              <Card.Image
                src={item.images[0]}
                objectFit="cover"
                width="100%"
                height="100%"
                alt={item.title}
              />
            </Col>
            <Col>
              <Text h2>{item.title}</Text>
              <Text size={"$xl"}>{item.description}</Text>
            </Col>
            <Col css={{ width: "auto" }}>
              <Text
                css={{
                  color: "$accents7",
                  fontWeight: "$semibold",
                }}
                size={"$2xl"}
              >
                ${item.price}
              </Text>
            </Col>
          </Row>
        </Card.Body>
        <Card.Footer>
          <Row justify={"space-between"}>
            <Col>
              <Button
                color={"error"}
                icon={<TrashIcon width={24} />}
                onClick={() => {
                  setCart({ type: "delete", product: item });
                }}
              >
                Remove
              </Button>
            </Col>
            <Col css={{ width: "auto" }}>
              <Button.Group>
                <Button
                  onClick={() => {
                    setCart({ type: "remove", product: item });
                  }}
                >
                  -
                </Button>
                <Button>{item.quantity}</Button>
                <Button
                  onClick={() => {
                    setCart({ type: "add", product: item });
                  }}
                >
                  +
                </Button>
              </Button.Group>
            </Col>
          </Row>
        </Card.Footer>
      </Card>
    </Grid>
  );
};
export default CartItem;
Enter fullscreen mode Exit fullscreen mode

In this component, we import useDispatchCart in order to update the cart state from each cart item. We can use setCart() to add, remove, and delete cart items by providing the action type and product.

Adding the Carts page

To add the Carts page, create a new file called ./pages/cart.js:

// ./pages/cart.js
import { Container, Grid, Spacer, Text } from "@nextui-org/react";
import Head from "next/head";
import { useCart } from "../modules/AppContext";
import CartItem from "../components/CartItem";
const Cart = () => {
  const cart = useCart();
  return (
    <>
      <Head>
        <title>Cart | Shopping app</title>
      </Head>
      <main>
        <Spacer y={2} />
        <Container lg>
          <header>
            <Text h1>Cart</Text>
          </header>
          <section className="cart-items">
            <Grid.Container gap={2}>
              {cart.map((item) => (
                <CartItem item={item} key={item.id} />
              ))}
            </Grid.Container>
          </section>
        </Container>
      </main>
    </>
  );
};
export default Cart;
Enter fullscreen mode Exit fullscreen mode

With that, we should have this: NextUI and Next.js App Final Product

Conclusion

So far, we’ve been able to build out a simple ecommerce application with NextUI as a UI framework. We’ve covered some features, including a few important components like Navbar, Button, and more.

We also covered how to customize individual components using the css prop. Customization can also be applied globally by customizing the theme.

Having covered all that, we’ll be able to extend the features of the ecommerce app and build out other applications using NextUI. Check out the official docs to learn more about NextUI and some great examples for React and Next.js.


LogRocket: Full visibility into production Next.js apps

Debugging Next applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.

LogRocket signup

LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your Next app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.

The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.

Modernize how you debug your Next.js apps — start monitoring for free.

Latest comments (0)