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.

0. Tailwind CSS as an optional add-on

We will be using Tailwind CSS as an optional CSS framework for our demo dashboard, mostly to handle minor layout considerations for different viewports. Note that Tailwind CSS is not required to run tremor. This means you can also mimic the corresponding code with native CSS.

 

We install Tailwind CSS using npm and initialize the configuration files via the terminal.

npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p

We add the following paths to our tailwind.config.js file.

/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./app/**/*.{js,ts,jsx,tsx}",
    "./pages/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

We add the following directives to our globals.css file.

@tailwind base;
@tailwind components;
@tailwind utilities;

To use the icon imports in our dashboard we install Heroicons. To get the same icons from the examples, we install version 1.0.6.

npm i heroicons@1.0.6

1. Load a page shell

Our demo dashboard is based on one of our page shell blocks. They are perfectly suited 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). If you are still interested in using them in your first dashboard, have a look at the GitHub repository of the demo dashboard.

 

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 sadipscing elitr.

import { useState } from 'react';
import {
    Block,
    Card,
    ColGrid,
    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 } handleSelect={ (value) => setSelectedView(value) } marginTop="mt-6">
                <Tab value={ 1 } text="Overview" />
                <Tab value={ 2 } text="Detail" />
            </TabList>

            { selectedView === 1 ? (
                <>
                    <ColGrid numColsMd={ 2 } numColsLg={ 3 } gapX="gap-x-6" gapY="gap-y-6" marginTop="mt-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>
                    </ColGrid>

                    <Block marginTop="mt-6">
                        <Card>
                            <div className="h-80" />
                        </Card>
                    </Block>
                </>
            ) : (
                <Block marginTop="mt-6">
                    <Card>
                        <div className="h-96" />
                    </Card>
                </Block>
            ) }
        </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,
    Block,
    Card,
    Flex,
    Metric,
    ProgressBar,
    Text,
} from '@tremor/react';

// Single KPI card in the demo dashboard with sample inputs
export default function KpiCard() {
    return (
        <Card maxWidth="max-w-lg">
            <Flex alignItems="items-start">
                <Block>
                    <Text>Sales</Text>
                    <Metric>$ 12,699</Metric>
                </Block>
                <BadgeDelta deltaType="moderateIncrease" text="13.2%" />
            </Flex>
            <Flex marginTop="mt-4">
                <Text truncate={ true }>
                    68% ($ 149,940)
                </Text>
                <Text> $ 220,500 </Text>
            </Flex>
            <ProgressBar percentageValue={ 15.9 } marginTop="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,
    Block,
    Card,
    ColGrid,
    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 (
        <ColGrid numColsMd={ 2 } numColsLg={ 3 } marginTop="mt-6" gapX="gap-x-6" gapY="gap-y-6">
            { kpiData.map((item) => (
                <Card key={ item.title }>
                    <Flex alignItems="items-start">
                        <Block truncate={ true }>
                            <Text>{ item.title }</Text>
                            <Metric truncate={ true }>{ item.metric }</Metric>
                        </Block>
                        <BadgeDelta deltaType={ item.deltaType } text={ item.delta } />
                    </Flex>
                    <Flex marginTop="mt-4" spaceX="space-x-2">
                        <Text truncate={ true }>{ `${item.progress}% (${item.metric})` }</Text>
                        <Text>{ item.target }</Text>
                    </Flex>
                    <ProgressBar percentageValue={ item.progress } marginTop="mt-2" />
                </Card>
            )) }
        </ColGrid>
    );
}

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,
    Block,
    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">
                <Block>
                    <Flex justifyContent="justify-start" spaceX="space-x-0.5" alignItems="items-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>
                </Block>
                <div className="mt-6 md:mt-0">
                    <Toggle
                        color="zinc"
                        defaultValue={ selectedKpi }
                        handleSelect={ (value) => setSelectedKpi(value) }
                    >
                        <ToggleItem value="Sales" text="Sales" />
                        <ToggleItem value="Profit" text="Profit" />
                        <ToggleItem value="Customers" text="Customers" />
                    </Toggle>
                </div>
            </div>
            <AreaChart
                data={ performance }
                dataKey="date"
                categories={ [selectedKpi] }
                colors={ ['blue'] }
                showLegend={ false }
                valueFormatter={ formatters[selectedKpi] }
                yAxisWidth="w-14"
                height="h-96"
                marginTop="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,
    Flex,
    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:justify-start sm:space-x-2">
                <MultiSelectBox
                    handleSelect={ (value) => setSelectedNames(value) }
                    placeholder="Select Salespeople"
                    maxWidth="max-w-xs"
                >
                    { salesPeople.map((item) => (
                        <MultiSelectBoxItem key={ item.name } value={ item.name } text={ item.name } />
                    )) }
                </MultiSelectBox>
                <Dropdown
                    maxWidth="max-w-xs"
                    defaultValue="all"
                    handleSelect={ (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
                    handleSelect={ (value) => setSelectedNames(value) }
                    placeholder="Select Salespeople"
                    maxWidth="max-w-full"
                >
                    { salesPeople.map((item) => (
                        <MultiSelectBoxItem key={ item.name } value={ item.name } text={ item.name } />
                    )) }
                </MultiSelectBox>
                <Dropdown
                    maxWidth="max-w-full"
                    defaultValue="all"
                    handleSelect={ (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 marginTop="mt-6">
                <TableHead>
                    <TableRow>
                        <TableHeaderCell>Name</TableHeaderCell>
                        <TableHeaderCell textAlignment="text-right">Leads</TableHeaderCell>
                        <TableHeaderCell textAlignment="text-right">Sales ($)</TableHeaderCell>
                        <TableHeaderCell textAlignment="text-right">Quota ($)</TableHeaderCell>
                        <TableHeaderCell textAlignment="text-right">Variance</TableHeaderCell>
                        <TableHeaderCell textAlignment="text-right">Region</TableHeaderCell>
                        <TableHeaderCell textAlignment="text-right">Status</TableHeaderCell>
                    </TableRow>
                </TableHead>

                <TableBody>
                    { salesPeople.filter((item) => isSalesPersonSelected(item)).map((item) => (
                        <TableRow key={ item.name }>
                            <TableCell>{ item.name }</TableCell>
                            <TableCell textAlignment="text-right">{ item.leads }</TableCell>
                            <TableCell textAlignment="text-right">{ item.sales }</TableCell>
                            <TableCell textAlignment="text-right">{ item.quota }</TableCell>
                            <TableCell textAlignment="text-right">{ item.variance }</TableCell>
                            <TableCell textAlignment="text-right">{ item.region }</TableCell>
                            <TableCell textAlignment="text-right">
                                <BadgeDelta deltaType={ item.deltaType } text={ item.status } size="xs" />
                            </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.

Hint: Although deployment is not the focus of tremor, we can highly recommend using Vercel if you have little to no experience with deployment. Simply provide a GitHub repository and Vercel will guide you through the steps. A free plan is also available for personal or non-commercial projects.