Skip to content
Docs
Water Tracker

Water Tracker

Track daily water intake with a glanceable fill bar and one-tap top-ups.

requires interactionclick
Loading...

How it works

dailyGoal and sipMl set the target and increment size. Use defaultIntake for an uncontrolled demo, or intake with onIntakeChange when your app owns hydration state. The right edge is a CSS height transition — no chart library — so the tile stays lightweight for bento grids.

Installation

CLI

pnpm dlx shadcn@latest add https://animata.design/r/widget/water-tracker.json

Manual

Install dependencies

npm install lucide-react

Run the following command

It will create a new file called water-tracker.tsx inside the components/animata/widget directory.

mkdir -p components/animata/widget && touch components/animata/widget/water-tracker.tsx

Paste the code

Open the newly created file and paste the following code:

"use client";
 
import { Droplet } from "lucide-react";
import { useState } from "react";
 
import { cn } from "@/lib/utils";
 
export type WaterTrackerProps = {
  className?: string;
  dailyGoal?: number;
  sipMl?: number;
  defaultIntake?: number;
  intake?: number;
  onIntakeChange?: (ml: number) => void;
};
 
export default function WaterTracker({
  className,
  dailyGoal = 2000,
  sipMl = 250,
  defaultIntake = 750,
  intake: controlledIntake,
  onIntakeChange,
}: WaterTrackerProps) {
  const [internalIntake, setInternalIntake] = useState(defaultIntake);
  const intake = controlledIntake ?? internalIntake;
  const atGoal = intake >= dailyGoal;
  const progress = Math.min(100, (intake / dailyGoal) * 100);
 
  const addWater = () => {
    if (atGoal) return;
    const next = Math.min(dailyGoal, intake + sipMl);
    if (controlledIntake === undefined) {
      setInternalIntake(next);
    }
    onIntakeChange?.(next);
  };
 
  return (
    <div
      className={cn(
        "relative flex size-52 overflow-hidden rounded-3xl border border-border bg-card font-sans shadow-md",
        className,
      )}
    >
      <div className="flex min-w-0 flex-1 flex-col p-4">
        <div className="flex shrink-0 items-center gap-1.5">
          <Droplet className="size-[15px] fill-sky-500 text-sky-500" aria-hidden />
          <p className="text-[15px] font-semibold leading-none text-foreground">Water</p>
        </div>
 
        <div className="mt-3 flex flex-col gap-1">
          <p className="text-[22px] font-normal leading-none tabular-nums tracking-tight text-foreground">
            {intake.toLocaleString()}
            <span className="text-[13px] font-medium text-muted-foreground"> ml</span>
          </p>
          <p className="text-[13px] leading-none text-muted-foreground">
            of {dailyGoal.toLocaleString()} ml today
          </p>
        </div>
 
        <button
          type="button"
          onClick={addWater}
          disabled={atGoal}
          className={cn(
            "touch-manipulation mt-auto min-h-11 w-full rounded-full text-[15px] font-medium leading-none transition-transform",
            atGoal
              ? "cursor-not-allowed bg-muted text-muted-foreground"
              : "bg-sky-600 text-white hover:scale-[1.02] active:scale-[0.98]",
          )}
        >
          +{sipMl} ml
        </button>
      </div>
 
      <div className="relative w-10 shrink-0 border-l border-border bg-muted/30" aria-hidden>
        <div
          className="absolute inset-x-0 bottom-0 bg-linear-to-t from-sky-600 to-sky-400 transition-[height] duration-300 ease-out"
          style={{ height: `${progress}%` }}
        />
      </div>
    </div>
  );
}

Credits

Built by Sumin Gurung