Componenteditors1.16 KB gzipZero dependencies

Feature Flag Panel

Feature flag panel with toggle switches and rollout percentages.

Preview

Live Preview

Dark mode

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add feature-flag-panel

Or install the full npm package:

npm install bigbullui
import { FeatureFlagPanel } from "@/components/ui/feature-flag-panel";

<FeatureFlagPanel />

Usage guidance

Use Feature Flag Panel when a ticket stub surface needs Feature flag panel with toggle switches and rollout percentages. Keep props minimal and prefer composition with sibling stubs.

Avoid Feature Flag Panel for long form flows or dense data grids where a dedicated layout block fits better.

Source code

feature-flag-panel.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface FeatureFlag {
  id: string;
  label: string;
  enabled: boolean;
  /** Gradual rollout percentage */
  rollout?: number;
}

export interface FeatureFlagPanelProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "onToggle"> {
  flags: FeatureFlag[];
  onToggle?: (id: string, enabled: boolean) => void;
}

/** Feature flag management panel: toggles + rollout percentages. */
export function FeatureFlagPanel({ flags, onToggle, className, ...props }: FeatureFlagPanelProps) {
  const [state, setState] = React.useState(flags);

  const toggle = (id: string) => {
    const next = state.map((f) => (f.id === id ? { ...f, enabled: !f.enabled } : f));
    setState(next);
    const changed = next.find((f) => f.id === id);
    if (changed) onToggle?.(changed.id, changed.enabled);
  };

  return (
    <div className={cn("w-full max-w-md divide-y divide-border/60 rounded-lg border border-border bg-card", className)} {...props}>
      <style>{`@keyframes ffSlide { from { opacity: 0; transform: translateX(-6px); } to { opacity: 1; transform: translateX(0); } }`}</style>
      {state.map((f, idx) => (
        <div key={f.id} className="flex items-center gap-3 px-4 py-3 animate-[ffSlide_0.25s_ease-out_both] motion-reduce:animate-none" style={{ animationDelay: `${idx * 50}ms` }}>
          <div className="min-w-0 flex-1">
            <p className="text-sm font-medium">{f.label}</p>
            {f.rollout !== undefined && (
              <div className="mt-1 flex items-center gap-2">
                <div className="h-1 w-20 overflow-hidden rounded-full bg-border/60">
                  <div className="h-full rounded-full bg-accent transition-all duration-500 motion-reduce:transition-none" style={{ width: `${f.rollout}%` }} />
                </div>
                <span className="font-mono text-[9px] tabular-nums text-muted-foreground">%{f.rollout} rollout</span>
              </div>
            )}
          </div>
          <button
            role="switch"
            aria-checked={f.enabled}
            aria-label={f.label}
            onClick={() => toggle(f.id)}
            className={cn(
              "relative h-5 w-9 shrink-0 rounded-full border transition-colors duration-200 motion-reduce:transition-none",
              "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
              f.enabled ? "border-accent bg-accent" : "border-border bg-secondary"
            )}
          >
            <span
              className={cn(
                "absolute top-0.5 size-3.5 rounded-full bg-background shadow-xs transition-all duration-200 motion-reduce:transition-none",
                f.enabled ? "left-[18px]" : "left-0.5"
              )}
            />
          </button>
        </div>
      ))}
    </div>
  );
}

Props

PropTypeDescription
flagsFeatureFlag[]
onToggle(id: string, enabled: boolean) => void