Timer
A shadcn-style timer component built with Ark UI primitives.
05
minutes00
secondsimport { PauseIcon, PlayIcon, RotateCcwIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Timer,
TimerActionTrigger,
TimerControl,
TimerDigits,
} from "@/components/ui/timer";
const TimerCountdownDemo = () => (
<Timer countdown startMs={5 * 60 * 1000}>
<TimerDigits
parts={[
{ type: "minutes", label: "minutes" },
{ type: "seconds", label: "seconds" },
]}
/>
<TimerControl>
<TimerActionTrigger action="start" asChild>
<Button size="sm" type="button" variant="outline">
<PlayIcon className="size-4" /> Start
</Button>
</TimerActionTrigger>
<TimerActionTrigger action="pause" asChild>
<Button size="sm" type="button" variant="outline">
<PauseIcon className="size-4" /> Pause
</Button>
</TimerActionTrigger>
<TimerActionTrigger action="reset" asChild>
<Button size="sm" type="button" variant="outline">
<RotateCcwIcon className="size-4" /> Reset
</Button>
</TimerActionTrigger>
</TimerControl>
</Timer>
);
export default TimerCountdownDemo;
Installation
npx shadcn@latest add @ark-cn/timerInstall the dependency required by this primitive:
npm install @ark-ui/reactCopy the component source into your app:
TSXcomponents/ui/timer.tsx
"use client";
import {
Timer as TimerPrimitive,
useTimer,
useTimerContext,
} from "@ark-ui/react/timer";
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
export type TimerProps = TimerPrimitive.RootProps;
export const Timer = ({ className, ...props }: TimerProps) => (
<TimerPrimitive.Root
className={cn("flex flex-col gap-4", className)}
data-slot="timer"
{...props}
/>
);
export type TimerAreaProps = TimerPrimitive.AreaProps;
export const TimerArea = ({ className, ...props }: TimerAreaProps) => (
<TimerPrimitive.Area
className={cn("flex items-center gap-2", className)}
data-slot="timer-area"
{...props}
/>
);
export type TimerItemProps = TimerPrimitive.ItemProps;
export const TimerItem = ({ className, ...props }: TimerItemProps) => (
<TimerPrimitive.Item
className={cn(
"min-w-[2ch] text-center font-mono text-2xl font-semibold",
className,
)}
data-slot="timer-item"
{...props}
/>
);
export type TimerSeparatorProps = TimerPrimitive.SeparatorProps;
export const TimerSeparator = ({
className,
...props
}: TimerSeparatorProps) => (
<TimerPrimitive.Separator
className={cn("text-xl text-muted-foreground", className)}
data-slot="timer-separator"
{...props}
/>
);
export type TimerControlProps = TimerPrimitive.ControlProps;
export const TimerControl = ({ className, ...props }: TimerControlProps) => (
<TimerPrimitive.Control
className={cn("flex flex-wrap items-center gap-2", className)}
data-slot="timer-control"
{...props}
/>
);
export type TimerActionTriggerProps = TimerPrimitive.ActionTriggerProps;
export const TimerActionTrigger = ({
className,
...props
}: TimerActionTriggerProps) => (
<TimerPrimitive.ActionTrigger
className={cn(className)}
data-slot="timer-action-trigger"
{...props}
/>
);
export type TimerDigitsPart = {
type: NonNullable<TimerPrimitive.ItemProps["type"]>;
label: string;
};
export type TimerDigitsProps = {
parts: TimerDigitsPart[];
separator?: ReactNode;
};
export const TimerDigits = ({ parts, separator = ":" }: TimerDigitsProps) => (
<TimerArea>
{parts.map((part, index) => (
<div className="flex items-center gap-2" key={`${part.type}-${index}`}>
<div className="flex flex-col items-center">
<TimerItem type={part.type} />
<span className="text-[11px] uppercase tracking-wider text-muted-foreground">
{part.label}
</span>
</div>
{index < parts.length - 1 ? (
<TimerSeparator>{separator}</TimerSeparator>
) : null}
</div>
))}
</TimerArea>
);
export const TimerContext = TimerPrimitive.Context;
export const TimerRootProvider = (props: TimerPrimitive.RootProviderProps) => (
<TimerPrimitive.RootProvider data-slot="timer-root-provider" {...props} />
);
export { useTimer, useTimerContext };
Update import aliases to match your project setup.
Usage
import * as Timer from "@/components/ui/timer"Read exported parts in src/components/ui/timer.tsx and compose the primitive according to the Ark UI pattern for this component.
Examples
Basic
00
days00
hours40
minutes00
secondsimport { PauseIcon, PlayIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Timer,
TimerActionTrigger,
TimerControl,
TimerDigits,
} from "@/components/ui/timer";
const TimerBasicDemo = () => (
<Timer startMs={40 * 60 * 1000} targetMs={60 * 60 * 1000}>
<TimerDigits
parts={[
{ type: "days", label: "days" },
{ type: "hours", label: "hours" },
{ type: "minutes", label: "minutes" },
{ type: "seconds", label: "seconds" },
]}
/>
<TimerControl>
<TimerActionTrigger action="start" asChild>
<Button size="sm" type="button" variant="outline">
<PlayIcon className="size-4" /> Play
</Button>
</TimerActionTrigger>
<TimerActionTrigger action="resume" asChild>
<Button size="sm" type="button" variant="outline">
<PlayIcon className="size-4" /> Resume
</Button>
</TimerActionTrigger>
<TimerActionTrigger action="pause" asChild>
<Button size="sm" type="button" variant="outline">
<PauseIcon className="size-4" /> Pause
</Button>
</TimerActionTrigger>
</TimerControl>
</Timer>
);
export default TimerBasicDemo;
Countdown
05
minutes00
secondsimport { PauseIcon, PlayIcon, RotateCcwIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Timer,
TimerActionTrigger,
TimerControl,
TimerDigits,
} from "@/components/ui/timer";
const TimerCountdownDemo = () => (
<Timer countdown startMs={5 * 60 * 1000}>
<TimerDigits
parts={[
{ type: "minutes", label: "minutes" },
{ type: "seconds", label: "seconds" },
]}
/>
<TimerControl>
<TimerActionTrigger action="start" asChild>
<Button size="sm" type="button" variant="outline">
<PlayIcon className="size-4" /> Start
</Button>
</TimerActionTrigger>
<TimerActionTrigger action="pause" asChild>
<Button size="sm" type="button" variant="outline">
<PauseIcon className="size-4" /> Pause
</Button>
</TimerActionTrigger>
<TimerActionTrigger action="reset" asChild>
<Button size="sm" type="button" variant="outline">
<RotateCcwIcon className="size-4" /> Reset
</Button>
</TimerActionTrigger>
</TimerControl>
</Timer>
);
export default TimerCountdownDemo;
Events
00
minutes00
secondsTicks: 0
import { PlayIcon, RotateCcwIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Timer,
TimerActionTrigger,
TimerControl,
TimerDigits,
} from "@/components/ui/timer";
const TimerEventsDemo = () => {
const [ticks, setTicks] = useState(0);
const [completed, setCompleted] = useState(false);
return (
<Timer
onComplete={() => setCompleted(true)}
onTick={() => setTicks((value) => value + 1)}
targetMs={60 * 1000}
>
<TimerDigits
parts={[
{ type: "minutes", label: "minutes" },
{ type: "seconds", label: "seconds" },
]}
/>
<TimerControl>
<TimerActionTrigger action="start" asChild>
<Button size="sm" type="button" variant="outline">
<PlayIcon className="size-4" /> Start
</Button>
</TimerActionTrigger>
<TimerActionTrigger action="reset" asChild>
<Button size="sm" type="button" variant="outline">
<RotateCcwIcon className="size-4" /> Reset
</Button>
</TimerActionTrigger>
</TimerControl>
<p className="text-xs text-muted-foreground">
Ticks: {ticks} {completed ? "· completed" : ""}
</p>
</Timer>
);
};
export default TimerEventsDemo;
Interval
00
seconds000
msimport { PauseIcon, PlayIcon, RotateCcwIcon } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Timer,
TimerActionTrigger,
TimerControl,
TimerDigits,
} from "@/components/ui/timer";
const TimerIntervalDemo = () => (
<Timer interval={100} targetMs={60 * 1000}>
<TimerDigits
parts={[
{ type: "seconds", label: "seconds" },
{ type: "milliseconds", label: "ms" },
]}
separator="."
/>
<TimerControl>
<TimerActionTrigger action="start" asChild>
<Button size="sm" type="button" variant="outline">
<PlayIcon className="size-4" /> Start
</Button>
</TimerActionTrigger>
<TimerActionTrigger action="pause" asChild>
<Button size="sm" type="button" variant="outline">
<PauseIcon className="size-4" /> Pause
</Button>
</TimerActionTrigger>
<TimerActionTrigger action="reset" asChild>
<Button size="sm" type="button" variant="outline">
<RotateCcwIcon className="size-4" /> Reset
</Button>
</TimerActionTrigger>
</TimerControl>
</Timer>
);
export default TimerIntervalDemo;
Pomodoro
Work session
25
minutes00
secondsCompleted cycles: 0
import { PauseIcon, PlayIcon, RotateCcwIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import {
Timer,
TimerActionTrigger,
TimerControl,
TimerDigits,
} from "@/components/ui/timer";
const TimerPomodoroDemo = () => {
const [isWorking, setIsWorking] = useState(true);
const [cycles, setCycles] = useState(0);
const onComplete = () => {
setIsWorking((value) => !value);
if (!isWorking) {
setCycles((value) => value + 1);
}
};
return (
<Timer
countdown
key={isWorking ? "work" : "break"}
onComplete={onComplete}
startMs={isWorking ? 25 * 60 * 1000 : 5 * 60 * 1000}
>
<p className="text-sm font-medium text-foreground">
{isWorking ? "Work session" : "Break session"}
</p>
<TimerDigits
parts={[
{ type: "minutes", label: "minutes" },
{ type: "seconds", label: "seconds" },
]}
/>
<TimerControl>
<TimerActionTrigger action="start" asChild>
<Button size="sm" type="button" variant="outline">
<PlayIcon className="size-4" /> Start
</Button>
</TimerActionTrigger>
<TimerActionTrigger action="pause" asChild>
<Button size="sm" type="button" variant="outline">
<PauseIcon className="size-4" /> Pause
</Button>
</TimerActionTrigger>
<TimerActionTrigger action="reset" asChild>
<Button size="sm" type="button" variant="outline">
<RotateCcwIcon className="size-4" /> Reset
</Button>
</TimerActionTrigger>
</TimerControl>
<p className="text-xs text-muted-foreground">
Completed cycles: {cycles}
</p>
</Timer>
);
};
export default TimerPomodoroDemo;
API reference
This component mirrors the upstream Ark UI primitive.
See the ARK UI documentation for the full API.