1. Getting Started
  2. Your First Dashboard

Getting Started

Create your first dashboard

Learn how to rebuild a light version of the demo dashboard as shown on the website in less than one hour.

Hint: Before you start with this section, make sure you have followed the installation guideline. Please note, that this demo relies on the Next.js app from the installation.

1. Load a page shell

Our demo dashboard is based on one of our page shell blocks. They are used to wrap visualizations and metrics into a visually compelling interface without taking care of layout or responsiveness considerations.

 

For simplicity, we neglect the integration of the datepicker and input elements (dropdown, multiselect).

 

In our case, we will go for the page shell with tabs and render the initial template code to get a feeling of the appearance. We will name our Tabs Overview and Detail.

Dashboard

Lorem ipsum dolor sit amet, consetetur.

import { useState } from "react";
import { Card, Grid, Tab, TabList, Text, Title } from "@tremor/react";

export default function KpiCardGrid() {
  const [selectedView, setSelectedView] = useState("1");
  return (
    <main className="bg-slate-50 p-6 sm:p-10">
      <Title>Dashboard</Title>
      <Text>Lorem ipsum dolor sit amet, consetetur sadipscing elitr.</Text>

      <TabList
        defaultValue="1"
        onValueChange={(value) => setSelectedView(value)}
        className="mt-6"
      >
        <Tab value="1" text="Overview" />
        <Tab value="2" text="Detail" />
      </TabList>

      {selectedView === "1" ? (
        <>
          <Grid numColsLg={3} className="mt-6 gap-6">
            <Card>
              {/* Placeholder to set height */}
              <div className="h-28" />
            </Card>
            <Card>
              {/* Placeholder to set height */}
              <div className="h-28" />
            </Card>
            <Card>
              {/* Placeholder to set height */}
              <div className="h-28" />
            </Card>
          </Grid>

          <div className="mt-6">
            <Card>
              <div className="h-80" />
            </Card>
          </div>
        </>
      ) : (
        <Card className="mt-6">
          <div className="h-96" />
        </Card>
      )}
    </main>
  );
}

2. Add KPI cards

For the overview page, we built simple KPI cards using the Metric, Badge and ProgressBar components. Below is the composition of one KPI card.

Sales

$ 12,699

13.2%

68% ($ 149,940)

$ 220,500

import {
  BadgeDelta,
  Card,
  Flex,
  Metric,
  ProgressBar,
  Text,
} from "@tremor/react";

// Single KPI card in the demo dashboard with sample inputs
export default function KpiCard() {
  return (
    <Card className="max-w-lg">
      <Flex alignItems="start">
        <div>
          <Text>Sales</Text>
          <Metric>$ 12,699</Metric>
        </div>
        <BadgeDelta deltaType="moderateIncrease">13.2%</BadgeDelta>
      </Flex>
      <Flex className="mt-4">
        <Text className="truncate">68% ($ 149,940)</Text>
        <Text> $ 220,500 </Text>
      </Flex>
      <ProgressBar percentageValue={15.9} className="mt-2" />
    </Card>
  );
}

In the case of several KPI cards as in our page shell, we use a data array and Map() method to only declare the KPI card composition once.

Hint: If the input in the KPI cards is relatively long, it is recommended to apply truncate in the corresponding components to show a potential overflow more aesthetically. If you are reading this page in desktop view, try shrinking the browser window and see how the truncate points appear at a small screen size.

Sales

$ 12,699

13.2%

15.9% ($ 12,699)

$ 80,000

Profit

$ 45,564

23.9%

36.5% ($ 45,564)

$ 125,000

Customers

1,072

10.1%

53.6% (1,072)

2,000

import {
  BadgeDelta,
  Card,
  Grid,
  DeltaType,
  Flex,
  Metric,
  ProgressBar,
  Text,
} from "@tremor/react";

type Kpi = {
  title: string;
  metric: string;
  progress: number;
  target: string;
  delta: string;
  deltaType: DeltaType;
};

const kpiData: Kpi[] = [
  {
    title: "Sales",
    metric: "$ 12,699",
    progress: 15.9,
    target: "$ 80,000",
    delta: "13.2%",
    deltaType: "moderateIncrease",
  },
  {
    title: "Profit",
    metric: "$ 45,564",
    progress: 36.5,
    target: "$ 125,000",
    delta: "23.9%",
    deltaType: "increase",
  },
  {
    title: "Customers",
    metric: "1,072",
    progress: 53.6,
    target: "2,000",
    delta: "10.1%",
    deltaType: "moderateDecrease",
  },
];

export default function KpiCardGrid() {
  return (
    <Grid numColsLg={3} className="mt-6 gap-6">
      {kpiData.map((item) => (
        <Card key={item.title}>
          <Flex alignItems="start">
            <div className="truncate">
              <Text>{item.title}</Text>
              <Metric className="truncate">{item.metric}</Metric>
            </div>
            <BadgeDelta deltaType={item.deltaType}>{item.delta}</BadgeDelta>
          </Flex>
          <Flex className="mt-4 space-x-2">
            <Text className="truncate">{`${item.progress}% (${item.metric})`}</Text>
            <Text>{item.target}</Text>
          </Flex>
          <ProgressBar percentageValue={item.progress} className="mt-2" />
        </Card>
      ))}
    </Grid>
  );
}

3. Add chart

For the last section of the overview page, we add an AreaChart component with a ToggleButton to hop between different data domains in the chart.

Hint: For the mobile view, we hide the y-axis to provide more space for the main content of the chart. This is why we introduce an additional code snippet for the chart but that is hidden when the page is called on a screen size larger than the mobile view.

Performance History

Daily increase or decrease per domain

import { useState } from "react";
import {
  AreaChart,
  Card,
  Flex,
  Icon,
  Text,
  Title,
  Toggle,
  ToggleItem,
} from "@tremor/react";
import { InformationCircleIcon } from "@heroicons/react/outline";

export const performance = [
  {
    date: "2021-01-01",
    Sales: 900.73,
    Profit: 173,
    Customers: 73,
  },
  {
    date: "2021-01-02",
    Sales: 1000.74,
    Profit: 174.6,
    Customers: 74,
  },
  // ...
  {
    date: "2021-03-13",
    Sales: 882,
    Profit: 682,
    Customers: 682,
  },
];

// Basic formatters for the chart values
const dollarFormatter = (value: number) =>
  `$ ${Intl.NumberFormat("us").format(value).toString()}`;

const numberFormatter = (value: number) =>
  `${Intl.NumberFormat("us").format(value).toString()}`;

export default function ChartView() {
  const [selectedKpi, setSelectedKpi] = useState("Sales");

  // map formatters by selectedKpi
  const formatters: { [key: string]: any } = {
    Sales: dollarFormatter,
    Profit: dollarFormatter,
    Customers: numberFormatter,
  };

  return (
    <Card>
      <div className="md:flex justify-between">
        <div>
          <Flex
            justifyContent="start"
            className="space-x-0.5"
            alignItems="center"
          >
            <Title> Performance History </Title>
            <Icon
              icon={InformationCircleIcon}
              variant="simple"
              tooltip="Shows day-over-day (%) changes of past performance"
            />
          </Flex>
          <Text> Daily increase or decrease per domain </Text>
        </div>
        <div className="mt-6 md:mt-0">
          <Toggle
            color="zinc"
            defaultValue={selectedKpi}
            onValueChange={(value) => setSelectedKpi(value)}
          >
            <ToggleItem value="Sales" text="Sales" />
            <ToggleItem value="Profit" text="Profit" />
            <ToggleItem value="Customers" text="Customers" />
          </Toggle>
        </div>
      </div>
      <AreaChart
        data={performance}
        index="date"
        categories={[selectedKpi]}
        colors={["blue"]}
        showLegend={false}
        valueFormatter={formatters[selectedKpi]}
        yAxisWidth={56}
        className="h-96 mt-8"
      />
    </Card>
  );
}

4. Add a table to the second page

For the second page, we have one big container in which we will use a table showing detailed information about the behavior of the sales performance.

NameLeadsSales ($)Quota ($)VarianceRegionStatus
Peter Doe451,000,0001,200,000lowRegion A

overperforming

Lena Whitehouse35900,0001,000,000lowRegion B

average

Phil Less52930,0001,000,000mediumRegion C

underperforming

John Camper22390,000250,000lowRegion A

overperforming

Max Balmoore49860,000750,000lowRegion B

overperforming

Peter Moore821,460,0001,500,000lowRegion A

average

Joe Sachs491,230,0001,800,000mediumRegion B

underperforming

import { useState } from 'react';
import {
    BadgeDelta,
    Card,
    DeltaType,
    Dropdown,
    DropdownItem,
    MultiSelectBox,
    MultiSelectBoxItem,
    Table,
    TableBody,
    TableCell,
    TableHead,
    TableHeaderCell,
    TableRow,
} from '@tremor/react';

export type SalesPerson = {
  name: string;
  leads: number;
  sales: string;
  quota: string;
  variance: string;
  region: string;
  status: string;
  deltaType: DeltaType;
};

export const salesPeople: SalesPerson[] = [
    {
        name: 'Peter Doe',
        leads: 45,
        sales: '1,000,000',
        quota: '1,200,000',
        variance: 'low',
        region: 'Region A',
        status: 'overperforming',
        deltaType: 'moderateIncrease',
    },
    {
        name: 'Lena Whitehouse',
        leads: 35,
        sales: '900,000',
        quota: '1,000,000',
        variance: 'low',
        region: 'Region B',
        status: 'average',
        deltaType: 'unchanged',
    },
    {
        name: 'Phil Less',
        leads: 52,
        sales: '930,000',
        quota: '1,000,000',
        variance: 'medium',
        region: 'Region C',
        status: 'underperforming',
        deltaType: 'moderateDecrease',
    },
    {
        name: 'John Camper',
        leads: 22,
        sales: '390,000',
        quota: '250,000',
        variance: 'low',
        region: 'Region A',
        status: 'overperforming',
        deltaType: 'increase',
    },
    {
        name: 'Max Balmoore',
        leads: 49,
        sales: '860,000',
        quota: '750,000',
        variance: 'low',
        region: 'Region B',
        status: 'overperforming',
        deltaType: 'increase',
    },
    {
        name: 'Peter Moore',
        leads: 82,
        sales: '1,460,000',
        quota: '1,500,000',
        variance: 'low',
        region: 'Region A',
        status: 'average',
        deltaType: 'unchanged',
    },
    {
        name: 'Joe Sachs',
        leads: 49,
        sales: '1,230,000',
        quota: '1,800,000',
        variance: 'medium',
        region: 'Region B',
        status: 'underperforming',
        deltaType: 'moderateDecrease',
    },
];

export default function TableView() {
    const [selectedStatus, setSelectedStatus] = useState('all');
    const [selectedNames, setSelectedNames] = useState<string[]>([]);

    const isSalesPersonSelected = (salesPerson: SalesPerson) => (salesPerson.status === selectedStatus || selectedStatus === 'all')
    && (selectedNames.includes(salesPerson.name) || selectedNames.length === 0);

    return (
        <Card>
            <div className="sm:mt-6 hidden sm:flex sm:start sm:space-x-2">
                <MultiSelectBox
                    onValueChange={ (value) => setSelectedNames(value) }
                    placeholder="Select Salespeople"
                    className="max-w-xs"
                >
                    { salesPeople.map((item) => (
                        <MultiSelectBoxItem
                            key={ item.name }
                            value={ item.name }
                            text={ item.name }
                        />
                    )) }
                </MultiSelectBox>
                <Dropdown
                    className="max-w-xs"
                    defaultValue="all"
                    onValueChange={ (value) => setSelectedStatus(value) }
                >
                    <DropdownItem value="all" text="All Performances" />
                    <DropdownItem value="overperforming" text="Overperforming" />
                    <DropdownItem value="average" text="Average" />
                    <DropdownItem value="underperforming" text="Underperforming" />
                </Dropdown>
            </div>
            <div className="mt-6 sm:hidden space-y-2 sm:space-y-0">
                <MultiSelectBox
                    onValueChange={ (value) => setSelectedNames(value) }
                    placeholder="Select Salespeople"
                    className="max-w-full"
                >
                    { salesPeople.map((item) => (
                        <MultiSelectBoxItem
                            key={ item.name }
                            value={ item.name }
                            text={ item.name }
                        />
                    )) }
                </MultiSelectBox>
                <Dropdown
                    className="max-w-full"
                    defaultValue="all"
                    onValueChange={ (value) => setSelectedStatus(value) }
                >
                    <DropdownItem value="all" text="All Performances" />
                    <DropdownItem value="overperforming" text="Overperforming" />
                    <DropdownItem value="average" text="Average" />
                    <DropdownItem value="underperforming" text="Underperforming" />
                </Dropdown>
            </div>

            <Table className="mt-6">
                <TableHead>
                    <TableRow>
                        <TableHeaderCell>Name</TableHeaderCell>
                        <TableHeaderCell className="text-right">Leads</TableHeaderCell>
                        <TableHeaderCell className="text-right">
                            Sales ($)
                        </TableHeaderCell>
                        <TableHeaderCell className="text-right">
                            Quota ($)
                        </TableHeaderCell>
                        <TableHeaderCell className="text-right">
                            Variance
                        </TableHeaderCell>
                        <TableHeaderCell className="text-right">Region</TableHeaderCell>
                        <TableHeaderCell className="text-right">Status</TableHeaderCell>
                    </TableRow>
                </TableHead>

                <TableBody>
                    { salesPeople
                        .filter((item) => isSalesPersonSelected(item))
                        .map((item) => (
                            <TableRow key={ item.name }>
                                <TableCell>{ item.name }</TableCell>
                                <TableCell className="text-right">{ item.leads }</TableCell>
                                <TableCell className="text-right">{ item.sales }</TableCell>
                                <TableCell className="text-right">{ item.quota }</TableCell>
                                <TableCell className="text-right">
                                    { item.variance }
                                </TableCell>
                                <TableCell className="text-right">{ item.region }</TableCell>
                                <TableCell className="text-right">
                                    <BadgeDelta deltaType={ item.deltaType } size="xs">
                                        { item.status }
                                    </BadgeDelta>
                                </TableCell>
                            </TableRow>
                        )) }
                </TableBody>
            </Table>
        </Card>
    );
}

That's it! Your first dashboard is ready to be shipped. If you have encountered any issues, please check out our GitHub site and raise an issue if no answer can be found.