Clean dismissible pills for roles and statuses, with Phosphor Fill icons and soft color variants.
Icons by Phosphor Icons (Fill weight). We use their icon set in this component — if you are able to, please consider supporting their work.
npx @loganbriggs/curated add pillimport {
CheckCircle,
Clock,
Crown,
ShieldCheck,
} from "@phosphor-icons/react";
import { Pill } from "@/components/curated/pill";
export function Example() {
return (
<div className="flex flex-wrap gap-3">
<Pill icon={Crown} variant="violet" dismissible onDismiss={() => {}}>
Owner
</Pill>
<Pill icon={ShieldCheck} variant="orange" dismissible onDismiss={() => {}}>
Admin
</Pill>
<Pill icon={CheckCircle} variant="emerald">
Active
</Pill>
<Pill icon={Clock} variant="amber" dismissible onDismiss={() => {}}>
Pending
</Pill>
</div>
);
}import * as React from "react";
import type { Icon } from "@phosphor-icons/react";
import { X } from "@phosphor-icons/react";
import { cn } from "@/lib/utils";
const pillVariants = {
rose: {
root: "bg-[#fff0f3]",
label: "text-[#ef476f]",
iconWrap: "bg-[#ef476f]",
icon: "text-white",
dismiss: "bg-[#ffc9d4] text-[#ef476f] hover:bg-[#ffb8c7]",
},
violet: {
root: "bg-[#f3ecff]",
label: "text-[#8338ec]",
iconWrap: "bg-[#8338ec]",
icon: "text-white",
dismiss: "bg-[#ddd0ff] text-[#8338ec] hover:bg-[#cfc0ff]",
},
orange: {
root: "bg-[#fff3eb]",
label: "text-[#fb5607]",
iconWrap: "bg-[#fb5607]",
icon: "text-white",
dismiss: "bg-[#ffd4b8] text-[#fb5607] hover:bg-[#ffc49e]",
},
emerald: {
root: "bg-[#ecfdf3]",
label: "text-[#059669]",
iconWrap: "bg-[#059669]",
icon: "text-white",
dismiss: "bg-[#b8ebd0] text-[#059669] hover:bg-[#a3e4c2]",
},
amber: {
root: "bg-[#fffbeb]",
label: "text-[#d97706]",
iconWrap: "bg-[#d97706]",
icon: "text-white",
dismiss: "bg-[#fde4a8] text-[#d97706] hover:bg-[#fcd88a]",
},
sky: {
root: "bg-[#eff8ff]",
label: "text-[#0284c7]",
iconWrap: "bg-[#0284c7]",
icon: "text-white",
dismiss: "bg-[#bfe3fa] text-[#0284c7] hover:bg-[#a8d9f8]",
},
} as const;
export type PillVariant = keyof typeof pillVariants;
export interface PillProps extends React.HTMLAttributes<HTMLSpanElement> {
icon: Icon;
variant?: PillVariant;
dismissible?: boolean;
onDismiss?: () => void;
}
export function Pill({
icon: IconComponent,
variant = "violet",
dismissible = false,
onDismiss,
className,
children,
...props
}: PillProps) {
const styles = pillVariants[variant];
return (
<span
className={cn(
"inline-flex h-[38px] select-none items-center rounded-full py-[3px] pl-[3px]",
"font-[ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,'Segoe_UI',sans-serif]",
dismissible ? "pr-[3px]" : "pr-4",
styles.root,
className,
)}
{...props}
>
<span
className={cn(
"flex size-[32px] shrink-0 items-center justify-center rounded-full",
styles.iconWrap,
)}
>
<IconComponent className={styles.icon} size={17} weight="fill" />
</span>
<span
className={cn(
"whitespace-nowrap px-2.5 text-[14px] font-semibold leading-none",
styles.label,
)}
>
{children}
</span>
{dismissible && (
<button
type="button"
aria-label={`Dismiss ${typeof children === "string" ? children : "pill"}`}
onClick={onDismiss}
className={cn(
"flex size-[26px] shrink-0 items-center justify-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-1",
styles.dismiss,
)}
>
<X size={12} weight="regular" />
</button>
)}
</span>
);
}