A feature-flag control with an on/off switch and a rollout-percentage slider; the affected-users count rolls on an odometer as the percentage changes.
npx shadcn@latest add @paragon/feature-flag-rowAlso installs: switch, slider, digit-roll
"use client";
import * as React from "react";
import { Switch } from "@/registry/paragon/ui/switch";
import { Slider } from "@/registry/paragon/ui/slider";
import { DigitRoll } from "@/registry/paragon/ui/digit-roll";
import { cn } from "@/lib/utils";
export interface FeatureFlagRowProps
extends Omit<React.ComponentProps<"div">, "onChange"> {
/** Flag key, shown in monospace. */
flagKey?: string;
/** Human description. */
description?: string;
/** Total audience the rollout percentage applies to. */
audience?: number;
/** Uncontrolled initial enabled state. */
defaultEnabled?: boolean;
/** Uncontrolled initial rollout percentage (0–100). */
defaultRollout?: number;
onChange?: (state: { enabled: boolean; rollout: number }) => void;
}
/**
* A feature-flag control: an on/off switch plus a rollout-percentage slider.
* The affected-users count is derived from percentage × audience and rolls on
* an odometer as you drag, so the blast radius of the change is always legible.
* Disabling the flag dims the rollout controls and drops the effective count to
* zero (rolling down). The slider stays keyboard-driven; the roll respects
* reduced motion via DigitRoll.
*/
export function FeatureFlagRow({
flagKey = "checkout.new_flow",
description = "New multi-step checkout with saved cards",
audience = 128_400,
defaultEnabled = true,
defaultRollout = 25,
onChange,
className,
...props
}: FeatureFlagRowProps) {
const [enabled, setEnabled] = React.useState(defaultEnabled);
const [rollout, setRollout] = React.useState(defaultRollout);
const affected = enabled ? Math.round((rollout / 100) * audience) : 0;
const emit = (next: { enabled: boolean; rollout: number }) =>
onChange?.(next);
return (
<div
data-slot="feature-flag-row"
className={cn(
"rounded-xl bg-card p-4 text-card-foreground shadow-border",
className,
)}
{...props}
>
<div className="flex items-start gap-3">
<div className="min-w-0 flex-1">
<code className="text-[13px] font-medium">{flagKey}</code>
<p className="mt-0.5 truncate text-xs text-muted-foreground">
{description}
</p>
</div>
<Switch
checked={enabled}
onCheckedChange={(v) => {
setEnabled(v);
emit({ enabled: v, rollout });
}}
aria-label={`Toggle ${flagKey}`}
/>
</div>
<div
className={cn(
"mt-4 transition-opacity duration-(--duration-base)",
enabled ? "opacity-100" : "opacity-50",
)}
>
<div className="flex items-center justify-between text-xs">
<span className="text-muted-foreground">Rollout</span>
<span className="font-medium tabular-nums">{rollout}%</span>
</div>
<Slider
className="mt-2"
value={[rollout]}
min={0}
max={100}
step={1}
disabled={!enabled}
ticks={[0, 25, 50, 75, 100]}
aria-label="Rollout percentage"
formatValue={(v) => `${v}%`}
onValueChange={([v]) => {
setRollout(v);
emit({ enabled, rollout: v });
}}
/>
<div className="mt-3 flex items-baseline gap-1.5 text-xs text-muted-foreground">
<DigitRoll
value={affected}
className="text-sm font-semibold text-foreground"
/>
<span>
of <span className="tabular-nums">{audience.toLocaleString()}</span>{" "}
users affected
</span>
</div>
</div>
</div>
);
}