Curated

Pill

Clean dismissible pills for roles and statuses, with Phosphor Fill icons and soft color variants.

Preview

OwnerAdminActivePendingVerifiedNew Member

Icons by Phosphor Icons (Fill weight). We use their icon set in this component — if you are able to, please consider supporting their work.

Install

npx @loganbriggs/curated add pill

Usage

import {
  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>
  );
}

Source

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>
  );
}