# Stats Dashboard with Progress Bars

> A stats dashboard with progress bars block.

A shadcn/ui block from the [@blocks-so](https://blocks.so) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/blocks-so/stats-11).

- Page, with a live preview: https://shadcn.exchange/r/blocks-so/stats-11
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/blocks-so/stats-11-light-d91cd5c8887bb95c.png
- Registry item: https://blocks.so/r/stats-11.json
- Categories: stats
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://blocks.so/r/stats-11.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`
- Registry items: `button`, `card`, `dialog`, `input`, `label`

## Files

### `content/components/stats/stats-11.tsx`

```tsx
'use client';

import { Box, Edit } from 'lucide-react';
import type React from 'react';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';

interface MetricCardProps {
  title: string;
  value: string;
  limit: string;
  percentage: number;
  status?: string;
  statusColor?: string;
  progressColor: string;
  details?: Array<{ label: string; value: string; color: string }>;
  actionLabel: string;
  actionIcon: React.ReactNode;
  warningMessage?: string;
  onActionClick?: () => void;
}

function MetricCard({
  title,
  value,
  limit,
  percentage,
  status,
  statusColor = 'text-emerald-600 dark:text-emerald-400',
  progressColor,
  details,
  actionLabel,
  actionIcon,
  warningMessage,
  onActionClick,
}: MetricCardProps) {
  const renderProgressBar = () => {
    if (details && title === 'Commands') {
      const writes = Number.parseInt(details[0].value.replace(/,/g, ''));
      const reads = Number.parseInt(details[1].value.replace(/,/g, ''));
      const total = writes + reads;
      const writesPercentage = (writes / total) * 100;
      const readsPercentage = (reads / total) * 100;

      return (
        <div className="relative h-1 w-full overflow-hidden rounded-full bg-muted">
          <div
            className="absolute left-0 h-full w-full origin-left bg-emerald-500 transition-transform duration-200 ease-out"
            style={{ transform: `scaleX(${writesPercentage / 100})` }}
          />
          <div
            className="absolute left-0 h-full w-full origin-left bg-blue-500 transition-transform duration-200 ease-out"
            style={{
              transform: `translateX(${writesPercentage}%) scaleX(${readsPercentage / 100})`,
            }}
          />
        </div>
      );
    }

    return (
      <div className="relative h-1 w-full overflow-hidden rounded-full bg-muted">
        <div
          className={`h-full w-full origin-left transition-transform duration-200 ease-out ${progressColor}`}
          style={{ transform: `scaleX(${Math.min(percentage, 100) / 100})` }}
        />
      </div>
    );
  };

  return (
    <Card className="relative max-w-[280px] overflow-hidden shadow-2xs">
      <CardContent className="p-4 py-0">
        <h5 className="font-normal text-muted-foreground text-xs uppercase leading-none tracking-wide dark:text-foreground/80">
          {title}
        </h5>

        <div className="mt-2 flex items-baseline gap-1">
          <div className="font-medium text-[1.2rem] text-foreground tabular-nums leading-none">
            {value}
          </div>
          <div className="text-muted-foreground text-xs leading-none">
            / {limit}
          </div>
        </div>

        <div className="mt-3">
          {renderProgressBar()}

          {details && (
            <div className="my-6 mb-8">
              <div className="flex flex-col gap-3">
                {details.map((detail, index) => (
                  <div
                    className="flex w-full items-center text-muted-foreground text-xs leading-none dark:text-foreground/70"
                    key={index}
                  >
                    <div
                      className={`mr-[6px] h-2 w-2 rounded-full ${detail.color}`}
                    />
                    <div className="mr-1">{detail.label}</div>
                    <div className="h-[9px] flex-1 border-border border-b-2 border-dotted" />
                    <div className="ml-1 tabular-nums">{detail.value}</div>
                  </div>
                ))}
              </div>
            </div>
          )}

          {status && (
            <div className="pt-2">
              <div className={statusColor}>{status}</div>
            </div>
          )}

          {warningMessage && (
            <div className="pt-2">
              <div className="text-amber-700 text-sm dark:text-amber-400">
                {warningMessage}
              </div>
            </div>
          )}
        </div>

        <div className="absolute right-0 bottom-0 left-0">
          <Button
            className="h-8 w-full justify-start gap-0 rounded-none bg-muted/50 text-blue-500 hover:text-blue-600 dark:text-blue-400 dark:hover:text-blue-300"
            onClick={onActionClick}
            variant="ghost"
          >
            {actionIcon}
            <span className="ml-1 text-xs">{actionLabel}</span>
          </Button>
        </div>
      </CardContent>
    </Card>
  );
}

function BudgetDialog({
  open,
  onOpenChange,
}: {
  open: boolean;
  onOpenChange: (open: boolean) => void;
}) {
  const [budget, setBudget] = useState('150');

  const handleUpdate = () => {
    onOpenChange(false);
  };

  return (
    <Dialog onOpenChange={onOpenChange} open={open}>
      <DialogContent className="sm:max-w-md">
        <DialogHeader>
          <DialogTitle>Update budget</DialogTitle>
          <DialogDescription>
            When your monthly cost reaches the max budget, we send an email and
            throttle your database. You will not be charged beyond your set
            budget for this database.
          </DialogDescription>
        </DialogHeader>

        <div className="space-y-2">
          <Label htmlFor="budget">Max budget per month</Label>
          <Input
            id="budget"
            onChange={(e) => setBudget(e.target.value)}
            placeholder="150"
            type="number"
            value={budget}
          />
        </div>

        <DialogFooter className="pt-2">
          <Button onClick={() => onOpenChange(false)} variant="outline">
            Cancel
          </Button>
          <Button onClick={handleUpdate}>Update</Button>
        </DialogFooter>
      </DialogContent>
    </Dialog>
  );
}

export default function Stats11() {
  const [budgetDialogOpen, setBudgetDialogOpen] = useState(false);

  return (
    <>
      <div className="grid grid-cols-1 gap-3 md:grid-cols-2 lg:grid-cols-4">
        <MetricCard
          actionIcon={<Box className="h-4 w-4" />}
          actionLabel="Upgrade"
          details={[
            { label: 'Writes', value: '11,276,493', color: 'bg-emerald-500' },
            { label: 'Reads', value: '2,548,921', color: 'bg-blue-500' },
          ]}
          limit="Unlimited"
          percentage={67}
          progressColor="bg-blue-500"
          title="Commands"
          value="13.8M"
        />

        <MetricCard
          actionIcon={<Box className="h-4 w-4" />}
          actionLabel="Upgrade"
          limit="150 GB"
          percentage={94}
          progressColor="bg-orange-500"
          title="Bandwidth"
          value="141 GB"
          warningMessage="There will be a charge for the excessive bandwidth over the limit."
        />

        <MetricCard
          actionIcon={<Box className="h-4 w-4" />}
          actionLabel="Upgrade"
          limit="500 GB"
          percentage={7.4}
          progressColor="bg-emerald-500"
          status="It's all right."
          title="Storage"
          value="37 GB"
        />

        <MetricCard
          actionIcon={<Edit className="h-4 w-4" />}
          actionLabel="Change Budget"
          limit="$150 Budget"
          onActionClick={() => setBudgetDialogOpen(true)}
          percentage={48.95}
          progressColor="bg-emerald-500"
          status="It's all right."
          title="Cost"
          value="$73.42"
        />
      </div>

      <BudgetDialog
        onOpenChange={setBudgetDialogOpen}
        open={budgetDialogOpen}
      />
    </>
  );
}
```
