Building a Responsive Dashboard UI with HeroUI and Next.js

Creating modern dashboards has never been easier. With the right tools, you can quickly build responsive, sleek, and functional UIs. In this tutorial, you’ll learn how to use HeroUI and Next.js to create an admin dashboard UI. We’ll focus on layout composition, icon integration, and responsiveness.

Getting Started: Project Setup and Tools

To begin, create a new Next.js project. Use your terminal and run:

npx heroui-cli@latest init -t app

Once inside the project folder, install npm:

npm install

Once inside the project folder, install Heroicons:

npm install @heroicons/react

Heroicons provide high-quality SVG icons as React components. This makes them perfect for modern UIs.

Once inside the project folder, install HeroUI:

npm install @heroui/react

Once inside the project folder, install recharts:

npm install recharts

Now that everything’s set, let’s build the foundation of your dashboard layout. A good UI begins with a clear structure.

Project directory structure

admin-dashboard/
├── public/
│   ├── favicon.ico
│   ├── logo.svg
│   └── index.html
├── app/
│   ├── layout.tsx
│   └── dashboard/
│       └── page.tsx
├── components/
│   ├── ChartCard.tsx
│   ├── RecentOrders.tsx
│   ├── Sidebar.tsx
│   └── StatsCard.tsx
├── types/
│   ├── ChartCardType.ts
└── └── StatsCardType.ts

Designing the Layout

A typical admin dashboard has two key areas: a sidebar and a main content panel. Start by creating a layout component under /components/Layout.js.

Here’s a basic layout structure:

import "@/styles/globals.css";
import { Metadata, Viewport } from "next";
import clsx from "clsx";
import { HeroUIProvider } from "@heroui/system";

import { Providers } from "./providers";

import { siteConfig } from "@/config/site";
import { fontSans } from "@/config/fonts";

export const metadata: Metadata = {
  title: {
    default: siteConfig.name,
    template: `%s - ${siteConfig.name}`,
  },
  description: siteConfig.description,
  icons: {
    icon: "/favicon.ico",
  },
};

export const viewport: Viewport = {
  themeColor: [
    { media: "(prefers-color-scheme: light)", color: "white" },
    { media: "(prefers-color-scheme: dark)", color: "black" },
  ],
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html suppressHydrationWarning lang="en">
      <head />
      <body
        className={clsx(
          "min-h-screen text-foreground bg-background font-sans antialiased",
          fontSans.variable,
        )}
      >
        <Providers themeProps={{ attribute: "class", defaultTheme: "dark" }}>
          <div className="relative flex flex-col h-screen">
            <HeroUIProvider>
              <main className="flex-grow p-6 overflow-auto">
                <div className="mx-auto max-w-7xl">{children}</div>
              </main>
            </HeroUIProvider>
          </div>
        </Providers>
      </body>
    </html>
  );
}

Sidebar Navigation

Clean sidebar with navigation items using NextUI buttons and icons

Create a file: /components/Sidebar.tsx. Use Heroicons here to make the navigation visually appealing:

"use client";

import { Card, CardBody, Button } from "@heroui/react";
import {
  HomeIcon,
  ChartBarIcon,
  Cog6ToothIcon,
  UserGroupIcon,
  ShoppingCartIcon,
} from "@heroicons/react/24/outline";

export const Sidebar = () => {
  const navItems = [
    { name: "Dashboard", icon: HomeIcon, key: "dashboard" },
    { name: "Analytics", icon: ChartBarIcon, key: "analytics" },
    { name: "Users", icon: UserGroupIcon, key: "users" },
    { name: "Orders", icon: ShoppingCartIcon, key: "orders" },
    { name: "Settings", icon: Cog6ToothIcon, key: "settings" },
  ];

  return (
    <Card className="w-64 h-screen rounded-none shadow-md">
      <CardBody className="p-0">
        <div className="p-4 text-lg font-bold border-b border-divider">
          Admin Panel
        </div>
        <div className="p-2">
          {navItems.map((item) => (
            <Button
              key={item.key}
              className="w-full justify-start mb-1 h-12"
              startContent={<item.icon className="h-5 w-5" />}
              variant="light"
            >
              {item.name}
            </Button>
          ))}
        </div>
      </CardBody>
    </Card>
  );
};

Stats Cards

Four key metrics with trend indicators and color-coded icons

//component/dashboard/StatsCard.tsx
"use client";

import { Card, CardBody } from "@heroui/react";
import { ArrowUpIcon, ArrowDownIcon } from "@heroicons/react/24/outline";

import { StatsCardProps } from "../../types/StatsCardType";

export const StatsCard = ({
  title,
  value,
  change,
  icon: Icon,
  color = "primary",
}: StatsCardProps) => (
  <Card className="h-full">
    <CardBody className="p-4">
      <div className="flex items-center justify-between">
        <div>
          <p className="text-sm text-default-600">{title}</p>
          <p className="text-2xl font-bold">{value}</p>
          <div className="flex items-center mt-1">
            {change > 0 ? (
              <ArrowUpIcon className="h-4 w-4 text-success" />
            ) : (
              <ArrowDownIcon className="h-4 w-4 text-danger" />
            )}
            <span
              className={`text-sm ml-1 ${change > 0 ? "text-success" : "text-danger"}`}
            >
              {Math.abs(change)}%
            </span>
          </div>
        </div>
        <div className={`p-3 rounded-full bg-${color}-100`}>
          <Icon className={`h-6 w-6 text-${color}-600`} />
        </div>
      </div>
    </CardBody>
  </Card>
);
//types/StatsCardType.ts
interface IconComponentProps {
  className?: string;
}
type IconType = React.ComponentType<IconComponentProps>;

export interface StatsCardProps {
  title: string;
  value: string;
  change: number;
  icon: IconType;
  color?: "primary" | "secondary" | "success" | "danger" | "warning";
}

Recent Orders Table

Complete table with customer avatars, status chips, and action dropdowns

//component/dashboard/RecentOrders.tsx
"use client";

import {
  Card,
  CardBody,
  CardHeader,
  Button,
  Chip,
  Table,
  TableHeader,
  TableColumn,
  TableBody,
  TableRow,
  TableCell,
  Avatar,
  Dropdown,
  DropdownTrigger,
  DropdownMenu,
  DropdownItem,
} from "@heroui/react";
import { EyeIcon, EllipsisVerticalIcon } from "@heroicons/react/24/outline";

export const RecentOrders = () => {
  const orders = [
    {
      id: "#12345",
      customer: "John Doe",
      amount: "$299.99",
      status: "completed",
      avatar: "https://i.pravatar.cc/150?u=1",
    },
    {
      id: "#12346",
      customer: "Jane Smith",
      amount: "$159.50",
      status: "pending",
      avatar: "https://i.pravatar.cc/150?u=2",
    },
    {
      id: "#12347",
      customer: "Mike Johnson",
      amount: "$89.99",
      status: "processing",
      avatar: "https://i.pravatar.cc/150?u=3",
    },
    {
      id: "#12348",
      customer: "Sarah Wilson",
      amount: "$449.00",
      status: "completed",
      avatar: "https://i.pravatar.cc/150?u=4",
    },
    {
      id: "#12349",
      customer: "Tom Brown",
      amount: "$199.99",
      status: "cancelled",
      avatar: "https://i.pravatar.cc/150?u=5",
    },
  ];

  const getStatusColor = (status: string) => {
    switch (status) {
      case "completed":
        return "success";
      case "pending":
        return "warning";
      case "processing":
        return "primary";
      case "cancelled":
        return "danger";
      default:
        return "default";
    }
  };

  return (
    <Card className="h-full">
      <CardHeader className="pb-3">
        <div className="flex justify-between items-center w-full">
          <h3 className="text-lg font-semibold">Recent Orders</h3>
          <Button
            endContent={<EyeIcon className="h-4 w-4" />}
            size="sm"
            variant="light"
          >
            View All
          </Button>
        </div>
      </CardHeader>
      <CardBody className="pt-0">
        <Table removeWrapper aria-label="Recent orders table">
          <TableHeader>
            <TableColumn>ORDER</TableColumn>
            <TableColumn>CUSTOMER</TableColumn>
            <TableColumn>AMOUNT</TableColumn>
            <TableColumn>STATUS</TableColumn>
            <TableColumn>ACTION</TableColumn>
          </TableHeader>
          <TableBody>
            {orders.map((order) => (
              <TableRow key={order.id}>
                <TableCell className="font-medium">{order.id}</TableCell>
                <TableCell>
                  <div className="flex items-center gap-2">
                    <Avatar size="sm" src={order.avatar} />
                    <span>{order.customer}</span>
                  </div>
                </TableCell>
                <TableCell className="font-medium">{order.amount}</TableCell>
                <TableCell>
                  <Chip
                    color={getStatusColor(order.status)}
                    size="sm"
                    variant="flat"
                  >
                    {order.status}
                  </Chip>
                </TableCell>
                <TableCell>
                  <Dropdown>
                    <DropdownTrigger>
                      <Button isIconOnly size="sm" variant="light">
                        <EllipsisVerticalIcon className="h-4 w-4" />
                      </Button>
                    </DropdownTrigger>
                    <DropdownMenu>
                      <DropdownItem key={"1"}>View Details</DropdownItem>
                      <DropdownItem key={"2"}>Edit Order</DropdownItem>
                      <DropdownItem key={"3"} className="text-danger">
                        Cancel Order
                      </DropdownItem>
                    </DropdownMenu>
                  </Dropdown>
                </TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </CardBody>
    </Card>
  );
};

ChartCard

//component/dashboard/ChartCard.tsx
"use client";
import { Card, CardBody, CardHeader } from "@heroui/react";

import { ChartCardProps } from "@/types/ChartCardType";

export const ChartCard = ({ title, children }: ChartCardProps) => (
  <Card className="h-full">
    <CardHeader>
      <h3 className="text-lg font-semibold">{title}</h3>
    </CardHeader>
    <CardBody className="pt-0">{children}</CardBody>
  </Card>
);
//types/ChartCardType.ts
export interface ChartCardProps {
  title: string;
  children: React.ReactNode;
}

Integrating the Dashboard Page

With the layout complete, it’s time to build the actual dashboard page. Use the Layout component to wrap your content.

//app/dashboard/page.tsx
"use client";

import {
  Card,
  CardBody,
  CardHeader,
  Button,
  Avatar,
  Input,
  Select,
  SelectItem,
  Badge,
  Modal,
  ModalContent,
  ModalHeader,
  ModalBody,
  ModalFooter,
  useDisclosure,
} from "@heroui/react";
import {
  UserGroupIcon,
  CurrencyDollarIcon,
  ShoppingCartIcon,
  EyeIcon,
  BellIcon,
  MagnifyingGlassIcon,
  ArrowUpIcon,
  ArrowDownIcon,
  PlusIcon,
} from "@heroicons/react/24/outline";
import {
  LineChart,
  Line,
  XAxis,
  YAxis,
  CartesianGrid,
  Tooltip,
  ResponsiveContainer,
  PieChart,
  Pie,
  Cell,
} from "recharts";

import { RecentOrders } from "@/components/dashboard/RecentOrders";
import { ChartCard } from "@/components/dashboard/ChartCard";
import { StatsCard } from "@/components/dashboard/StatsCard";
import { Sidebar } from "@/components/dashboard/Sidebar";

export default function AdminDashboard() {
  const { isOpen, onOpen, onClose } = useDisclosure();

  const salesData = [
    { name: "Jan", sales: 4000, revenue: 2400 },
    { name: "Feb", sales: 3000, revenue: 1398 },
    { name: "Mar", sales: 2000, revenue: 9800 },
    { name: "Apr", sales: 2780, revenue: 3908 },
    { name: "May", sales: 1890, revenue: 4800 },
    { name: "Jun", sales: 2390, revenue: 3800 },
    { name: "Jul", sales: 3490, revenue: 4300 },
  ];

  const categoryData = [
    { name: "Electronics", value: 400, color: "#0070f3" },
    { name: "Clothing", value: 300, color: "#7c3aed" },
    { name: "Home", value: 200, color: "#f59e0b" },
    { name: "Books", value: 100, color: "#ef4444" },
  ];

  const topProducts = [
    { name: "iPhone 15 Pro", sales: 1234, revenue: "$1,234,000", trend: 12 },
    { name: "MacBook Air M2", sales: 856, revenue: "$856,000", trend: 8 },
    { name: "AirPods Pro", sales: 2341, revenue: "$468,200", trend: -3 },
    { name: "iPad Pro", sales: 567, revenue: "$567,000", trend: 15 },
  ];

  return (
    <div className="min-h-screen bg-gray-50 flex">
      <Sidebar />
      <div className="flex-1 p-6">
        {/* Header */}
        <div className="flex justify-between items-center mb-6">
          <div>
            <h1 className="text-2xl font-bold">Dashboard</h1>
            <p className="text-default-600">Welcome back, Admin!</p>
          </div>
          <div className="flex items-center gap-3">
            <Input
              className="w-64"
              placeholder="Search..."
              startContent={<MagnifyingGlassIcon className="h-4 w-4" />}
            />
            <Badge color="danger" content="5">
              <Button isIconOnly variant="light">
                <BellIcon className="h-5 w-5" />
              </Button>
            </Badge>
            <Avatar src="https://i.pravatar.cc/150?u=admin" />
          </div>
        </div>

        {/* Stats Cards */}
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
          <StatsCard
            change={12.5}
            color="success"
            icon={CurrencyDollarIcon}
            title="Total Revenue"
            value="$54,239"
          />
          <StatsCard
            change={8.2}
            color="primary"
            icon={ShoppingCartIcon}
            title="Total Orders"
            value="1,423"
          />
          <StatsCard
            change={-2.1}
            color="warning"
            icon={UserGroupIcon}
            title="Total Users"
            value="12,847"
          />
          <StatsCard
            change={15.3}
            color="secondary"
            icon={EyeIcon}
            title="Page Views"
            value="89,342"
          />
        </div>

        {/* Main Content */}
        <div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-6">
          <div className="lg:col-span-2">
            <ChartCard title="Sales Overview">
              <ResponsiveContainer height={300} width="100%">
                <LineChart data={salesData}>
                  <CartesianGrid strokeDasharray="3 3" />
                  <XAxis dataKey="name" />
                  <YAxis />
                  <Tooltip />
                  <Line
                    dataKey="sales"
                    stroke="#0070f3"
                    strokeWidth={2}
                    type="monotone"
                  />
                  <Line
                    dataKey="revenue"
                    stroke="#7c3aed"
                    strokeWidth={2}
                    type="monotone"
                  />
                </LineChart>
              </ResponsiveContainer>
            </ChartCard>
          </div>

          <div>
            <ChartCard title="Sales by Category">
              <ResponsiveContainer height={300} width="100%">
                <PieChart>
                  <Pie
                    cx="50%"
                    cy="50%"
                    data={categoryData}
                    dataKey="value"
                    outerRadius={80}
                  >
                    {categoryData.map((entry, index) => (
                      <Cell key={`cell-${index}`} fill={entry.color} />
                    ))}
                  </Pie>
                  <Tooltip />
                </PieChart>
              </ResponsiveContainer>
              <div className="mt-4 space-y-2">
                {categoryData.map((item) => (
                  <div
                    key={item.name}
                    className="flex items-center justify-between"
                  >
                    <div className="flex items-center gap-2">
                      <div
                        className="w-3 h-3 rounded-full"
                        style={{ backgroundColor: item.color }}
                      />
                      <span className="text-sm">{item.name}</span>
                    </div>
                    <span className="text-sm font-medium">{item.value}</span>
                  </div>
                ))}
              </div>
            </ChartCard>
          </div>
        </div>

        {/* Bottom Section */}
        <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
          <RecentOrders />

          <Card>
            <CardHeader className="pb-3">
              <div className="flex justify-between items-center w-full">
                <h3 className="text-lg font-semibold">Top Products</h3>
                <Button size="sm" variant="light" onPress={onOpen}>
                  <PlusIcon className="h-4 w-4 mr-1" />
                  Add Product
                </Button>
              </div>
            </CardHeader>
            <CardBody className="pt-0">
              <div className="space-y-4">
                {topProducts.map((product, index) => (
                  <div
                    key={index}
                    className="flex items-center justify-between p-3 bg-default-50 rounded-lg"
                  >
                    <div className="flex-1">
                      <h4 className="font-medium">{product.name}</h4>
                      <p className="text-sm text-default-600">
                        {product.sales} sales
                      </p>
                    </div>
                    <div className="text-right">
                      <p className="font-medium">{product.revenue}</p>
                      <div className="flex items-center justify-end">
                        {product.trend > 0 ? (
                          <ArrowUpIcon className="h-4 w-4 text-success" />
                        ) : (
                          <ArrowDownIcon className="h-4 w-4 text-danger" />
                        )}
                        <span
                          className={`text-sm ml-1 ${product.trend > 0 ? "text-success" : "text-danger"}`}
                        >
                          {Math.abs(product.trend)}%
                        </span>
                      </div>
                    </div>
                  </div>
                ))}
              </div>
            </CardBody>
          </Card>
        </div>

        {/* Add Product Modal */}
        <Modal isOpen={isOpen} size="2xl" onClose={onClose}>
          <ModalContent>
            <ModalHeader>Add New Product</ModalHeader>
            <ModalBody>
              <div className="space-y-4">
                <Input label="Product Name" placeholder="Enter product name" />
                <div className="grid grid-cols-2 gap-4">
                  <Input label="Price" placeholder="$0.00" startContent="$" />
                  <Input label="Stock" placeholder="0" type="number" />
                </div>
                <Select label="Category" placeholder="Select category">
                  <SelectItem key="electronics">Electronics</SelectItem>
                  <SelectItem key="clothing">Clothing</SelectItem>
                  <SelectItem key="home">Home</SelectItem>
                  <SelectItem key="books">Books</SelectItem>
                </Select>
                <Input label="Description" placeholder="Product description" />
              </div>
            </ModalBody>
            <ModalFooter>
              <Button variant="light" onPress={onClose}>
                Cancel
              </Button>
              <Button color="primary" onPress={onClose}>
                Add Product
              </Button>
            </ModalFooter>
          </ModalContent>
        </Modal>
      </div>
    </div>
  );
}

Run the App

Start the development server:

npm run dev
dashboard
Add Product
Action button

Conclusion

With HeroUI and Next.js, building a clean and responsive dashboard is straightforward. You’ve learned to set up the project, create a layout, integrate icons, and ensure responsiveness. Best of all, the code is reusable and scalable.

This setup lays the groundwork for complex admin systems, analytics platforms, or SaaS dashboards. Continue exploring more components, such as charts and user tables, to enhance functionality.

The tools are powerful. Now it’s your turn to build something incredible.

This article was originally published on Medium.

8 thoughts on “Building a Responsive Dashboard UI with HeroUI and Next.js”

  1. free ai logo generator no watermark

    The article provides a comprehensive guide to building a dashboard UI with React, offering practical examples and clear explanations. The code snippets are helpful, but more real-world use cases would make it even more insightful.

  2. Netflix会员车位

    This article provides a great overview of building a dashboard with React components, making it easy to follow and implement. The examples are clear and helpful for understanding the layout and functionality.

  3. アイム ノット ヒューマン

    This article provides a comprehensive guide to building a dashboard UI with React, offering practical examples and reusable components. The code snippets are well-explained, making it easier to implement similar features in my own projects. Great resource for anyone looking to create a professional admin dashboard!

  4. sports basketball unblocked

    This article provides a comprehensive guide to building a dashboard with React, offering clear examples and practical insights. The code snippets are well-structured, making it easy to follow along. I particularly appreciate the detailed explanations of each component, which enhance understanding. The article is a valuable resource for developers looking to create professional dashboards.

  5. This article provides a great overview of building a dashboard with React and Tailwind CSS, especially the reusable component approach for stats cards and tables. The code examples are clear, but I wish there were more details on the backend integration for real data fetching. Overall, a helpful guide for developers looking to create admin panels!

  6. Đồng hồ đếm ngược

    This dashboard looks sleeker than my exs dating profile – plenty of stats but zero personal life! The charts are prettier than my attempts at origami, and the dropdown menus seem more useful than my Im busy text messages. However, Im slightly concerned that my own productivity metrics would probably get a cancelled status. Still, if these components could clean my apartment, Id be a happy admin!

  7. I couldnt help but laugh at how the code snippets are like a treasure map for tech wizards—full of aha! moments and whats that? questions. The dashboard looks sleeker than my neighbors new crypto portfolio, and I’m not even a developer! The sidebars and dropdowns alone could keep me entertained for hours. Honestly, it’s like reading a comedy sketch about UI design, where every component is a punchline waiting to happen.

  8. Tải đồng hồ đếm ngược

    This dashboard looks sleek but I bet the Cancel Order dropdown item is used more than the View Details one. Admin life is just one epic fail per day, right? The color-coding is great until youre colorblind and the charts are prettier than my attempts at budgeting. Why do all the icons look like theyre in a hurry? Maybe theyre just trying to keep up with the 89,342 page views! The modal for adding products feels like a will I ever use this again? kind of moment. Great work though, now wheres the Undo Everything button?

Leave a Comment

Your email address will not be published. Required fields are marked *