import { Fps } from "@/components/ui/fps";
export function FpsDemo() {
return (
<div className="relative h-80 w-full rounded-lg border bg-muted/50">
<Fps strategy="absolute" position="top-right" />
<div className="flex size-full flex-col items-center justify-center gap-1">
<div>Absolute positioning</div>
<div className="text-muted-foreground text-sm">
Relative to this container without a portal
</div>
</div>
</div>
);
}Installation
CLI
npx shadcn@latest add "https://diceui.com/r/fps"Manual
Copy and paste the following code into your project.
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import * as React from "react";
import * as ReactDOM from "react-dom";
import { cn } from "@/lib/utils";
const fpsVariants = cva(
"z-50 flex shrink-0 items-center gap-2 rounded-sm border bg-background/80 px-3 py-1.5 font-mono text-foreground text-sm backdrop-blur-sm",
{
variants: {
strategy: {
fixed: "fixed",
absolute: "absolute",
},
position: {
"top-left": "top-4 left-4",
"top-right": "top-4 right-4",
"bottom-left": "bottom-4 left-4",
"bottom-right": "right-4 bottom-4",
},
status: {
good: "text-primary",
warning: "text-orange-500",
error: "text-destructive",
},
},
defaultVariants: {
strategy: "fixed",
position: "top-right",
status: "good",
},
},
);
interface FpsProps
extends React.ComponentProps<"div">,
Omit<VariantProps<typeof fpsVariants>, "status"> {
label?: string;
updateInterval?: number;
warningThreshold?: number;
errorThreshold?: number;
portalContainer?: Element | DocumentFragment | null;
enabled?: boolean;
}
function Fps(props: FpsProps) {
const {
strategy = "fixed",
position = "top-right",
label,
updateInterval = 500,
warningThreshold = 30,
errorThreshold = 20,
portalContainer: portalContainerProp,
enabled = true,
className,
...fpsProps
} = props;
const [mounted, setMounted] = React.useState(false);
const [fps, setFps] = React.useState(0);
const frameCountRef = React.useRef(0);
const lastTimeRef = React.useRef(performance.now());
const animationFrameRef = React.useRef<number | null>(null);
const updateTimeoutRef = React.useRef<ReturnType<typeof setTimeout> | null>(
null,
);
React.useLayoutEffect(() => setMounted(true), []);
const status = React.useMemo(() => {
if (fps < errorThreshold) return "error";
if (fps < warningThreshold) return "warning";
return "good";
}, [fps, errorThreshold, warningThreshold]);
React.useEffect(() => {
if (!enabled || typeof window === "undefined") return;
function measureFps() {
const now = performance.now();
const delta = now - lastTimeRef.current;
frameCountRef.current += 1;
if (delta >= updateInterval) {
const currentFps = Math.round((frameCountRef.current * 1000) / delta);
setFps(currentFps);
frameCountRef.current = 0;
lastTimeRef.current = now;
}
animationFrameRef.current = requestAnimationFrame(measureFps);
}
animationFrameRef.current = requestAnimationFrame(measureFps);
return () => {
if (animationFrameRef.current !== null) {
cancelAnimationFrame(animationFrameRef.current);
}
if (updateTimeoutRef.current !== null) {
clearTimeout(updateTimeoutRef.current);
}
};
}, [enabled, updateInterval]);
if (!enabled) return null;
const portalContainer =
strategy === "absolute"
? null
: (portalContainerProp ?? (mounted ? globalThis.document?.body : null));
const Comp = (
<div
aria-hidden="true"
data-slot="fps"
{...fpsProps}
className={cn(fpsVariants({ strategy, position, status }), className)}
>
{label && (
<span data-slot="fps-label" className="text-muted-foreground">
{label}:
</span>
)}
<span data-slot="fps-value">{fps}</span>
</div>
);
return portalContainer ? ReactDOM.createPortal(Comp, portalContainer) : Comp;
}
export { Fps };Examples
Positioning Strategy
Control whether the FPS counter uses fixed or absolute positioning.
import { Fps } from "@/components/ui/fps";
export function FpsStrategyDemo() {
return (
<div className="flex w-full flex-col gap-4">
<div className="relative h-48 w-full rounded-lg border bg-muted/50 p-4">
<Fps strategy="absolute" position="top-right" label="Absolute" />
<div className="flex size-full flex-col items-center justify-center gap-1">
<div>Absolute positioning</div>
<div className="text-muted-foreground text-sm">
Relative to this container without a portal
</div>
</div>
</div>
<div className="relative h-48 w-full rounded-lg border bg-muted/50 p-4">
<Fps strategy="fixed" position="bottom-right" label="Fixed" />
<div className="flex size-full flex-col items-center justify-center gap-1">
<div>Fixed positioning</div>
<div className="text-muted-foreground text-sm">
Relative to viewport with a portal
</div>
</div>
</div>
</div>
);
}Custom Position
Choose from four corner positions for the FPS counter.
import { Fps } from "@/components/fps"
export default function App() {
return (
<div>
<Fps position="bottom-left" />
{/* Your app content */}
</div>
)
}Custom Thresholds
Configure warning and error thresholds for color-coded performance indicators.
import { Fps } from "@/components/fps"
export default function App() {
return (
<div>
<Fps
warningThreshold={45}
errorThreshold={30}
/>
{/* Your app content */}
</div>
)
}Conditional Rendering
Enable the FPS counter only in development environments.
import { Fps } from "@/components/fps"
export default function App() {
const isDevelopment = process.env.NODE_ENV === "development"
return (
<div>
<Fps enabled={isDevelopment} />
{/* Your app content */}
</div>
)
}API Reference
Fps
A component that displays a real-time FPS counter overlay.
Prop
Type
Features
- Real-time monitoring: Uses
requestAnimationFramefor accurate FPS measurement - Color-coded display: Automatically changes color based on performance thresholds
- Green: Good performance (above warning threshold)
- Yellow: Warning (below warning threshold)
- Red: Poor performance (below error threshold)
- Flexible positioning: Choose between
fixedandabsolutepositioning strategies - Customizable position: Choose from four corner positions
- Configurable update interval: Control how often the FPS value updates
- Performance optimized: Minimal overhead with efficient frame counting
Positioning Strategies
Fixed Positioning
When using strategy="fixed", the FPS counter is positioned relative to the viewport and rendered via a portal into the document body. This is useful when you want the counter to remain visible while scrolling.
<Fps strategy="fixed" position="top-right" />Absolute Positioning
When using strategy="absolute", the FPS counter is rendered directly in place (without a portal) and positioned relative to its nearest positioned ancestor. This is useful when you want the counter to be contained within a specific element with a relative wrapper.
<div className="relative">
<Fps strategy="absolute" position="bottom-left" />
</div>Note: Unlike fixed positioning, absolute positioning does not use a portal, so the component will respect relative positioning contexts and be contained within its parent element.
Performance Considerations
The FPS counter uses requestAnimationFrame to measure frame rate, which has minimal performance impact. The component:
- Only updates the display at the specified interval (default: 500ms)
- Uses refs to avoid unnecessary re-renders
- Automatically cleans up animation frames on unmount
For production builds, consider disabling the FPS counter:
<Fps enabled={process.env.NODE_ENV === "development"} />