Skip to content

Toggle Chips

Row of pill toggle chips with emoji; selected chips fill periwinkle and show a popping check. Multiple or single selection, controlled or not.

soft-flat/ui/toggle-chips
Open ↗

Source

"use client";

import { useState } from "react";
import { Check } from "lucide-react";
import { cn } from "@/lib/utils";

export interface ToggleChipsOption {
  value: string;
  label: string;
  emoji?: string;
}

export interface ToggleChipsProps {
  options?: ToggleChipsOption[];
  /** Allow several chips at once. */
  multiple?: boolean;
  value?: string[];
  defaultValue?: string[];
  onValueChange?: (value: string[]) => void;
  "aria-label"?: string;
  className?: string;
}

/** Row of pill toggle chips with emoji; selected chips fill with periwinkle and show a popping check. Single or multiple selection. */
export function ToggleChips({
  options = [
    { value: "design", label: "Design", emoji: "🎨" },
    { value: "dev", label: "Development", emoji: "💻" },
    { value: "marketing", label: "Marketing", emoji: "📣" },
    { value: "ops", label: "Operations", emoji: "⚙️" },
    { value: "sales", label: "Sales", emoji: "🤝" },
  ],
  multiple = true,
  value,
  defaultValue = ["design", "dev"],
  onValueChange,
  "aria-label": ariaLabel = "Teams",
  className,
}: ToggleChipsProps) {
  const [inner, setInner] = useState(defaultValue);
  const selected = value ?? inner;
  const toggle = (v: string) => {
    const next = selected.includes(v) ? selected.filter((x) => x !== v) : multiple ? [...selected, v] : [v];
    setInner(next);
    onValueChange?.(next);
  };

  return (
    <div role="group" aria-label={ariaLabel} className={cn("flex flex-wrap gap-2", className)}>
      {options.map((o) => {
        const on = selected.includes(o.value);
        return (
          <button
            key={o.value}
            type="button"
            aria-pressed={on}
            onClick={() => toggle(o.value)}
            className={cn(
              "da-focus inline-flex h-11 items-center gap-2 rounded-da-pill px-4 text-[15px] font-medium transition-[background-color,color,scale] duration-(--da-duration) ease-da-emphasized active:scale-95",
              on ? "bg-da-primary text-da-primary-fg" : "bg-da-surface-2 hover:bg-da-muted",
            )}
          >
            {on ? (
              <span className="grid size-5 place-items-center rounded-full bg-da-primary-fg/25 transition-[scale] duration-(--da-duration) ease-da-emphasized starting:scale-0">
                <Check aria-hidden className="size-3" strokeWidth={3} />
              </span>
            ) : (
              o.emoji && <span aria-hidden>{o.emoji}</span>
            )}
            {o.label}
          </button>
        );
      })}
    </div>
  );
}

export default ToggleChips;

modules/soft-flat/ui/toggle-chips/index.tsx

Props

PropTypeDefaultDescription
options{ value, label, emoji? }[]—Chips.
multiplebooleantrueMulti-select.
value / defaultValuestring[]—Selection.
onValueChange(v: string[]) => void—Handler.
aria-labelstring—Group label.

Other button variants in Soft Flat

Button in other art directions

Button

Pressable button with 6 variants (primary, secondary, ink, outline, ghost, danger), 4 sizes, icons, loading state and link mode (href). Hard shadow collapses on hover/press.

BrutalistButton

Copy Button

Code field with an attached yellow copy button that flips to green 'Copied' with a check; optional secret masking for API keys and custom display text for commands.

BrutalistButton

Segmented Control

Ruled segmented control for mutually exclusive options: mono uppercase segments split by strokes, ink selected segment, yellow hover. Controlled or uncontrolled, typed values.

BrutalistButton

Split Button

Yellow primary action glued to a chevron trigger that opens a ruled dropdown of secondary actions with descriptions (Radix DropdownMenu). Both halves flip to ink on hover.

BrutalistButton

Button

Pill button with 5 variants (primary→accent gradient, frosted glass, solid ink, ghost, danger), 4 sizes, icons, loading state and link mode. Lifts on hover with a slight overshoot.

GlassButton

Icon Button

Round frosted icon button with a tooltip; optional toggle mode (aria-pressed) that swaps the icon and fills with the gradient or danger color — ideal for call controls.

GlassButton

Record Button

Record toggle pill: red dot inside a soft halo that morphs into a stop square with a pulsing ring and a running mm:ss timer while recording.

GlassButton

Shine Button

Gradient pill button with a glossy top highlight and a bright light sweep crossing it on hover or keyboard focus (CSS only); renders an anchor when href is set. Two sizes.

GlassButton

Button

Compact button with 6 variants (indigo primary with inner highlight, ink secondary, outline, ghost, danger, link), 4 sizes, icons, keyboard-shortcut hint, loading state and link mode.

MinimalButton

Loading Button

Async action button that morphs through pending (spinner), success (green check) and error (red cross) states, then resets; all labels share one grid cell so the width never jumps.

MinimalButton

Shortcut Button

Button displaying its keyboard shortcut as keycaps; pressing the shortcut anywhere on the page clicks it (ignored while typing in a field). Primary, secondary and ghost styles.

MinimalButton

Toggle Group

Single-choice segmented toolbar on a muted track with a raised active segment (Radix ToggleGroup): text, icon-only or mixed options, two sizes, controlled or uncontrolled.

MinimalButton

Arrow Link

Text link with an ↗ that nudges up-right on hover and an ink underline that wipes in from the left; optional mono index prefix; scales from inline to display size.

Mono CleanButton

Button

Square monochrome button: ink primary, outlined (inverts on hover), ghost and underline (signal-colored underline) variants; three sizes and an optional trailing glyph.

Mono CleanButton

Icon Button

Square icon-only button with a hairline border (darkens on hover), ghost or solid ink variant; required label for aria-label and title.

Mono CleanButton

Text Toggle

Typographic toggle: options are plain mono words separated by slashes; the active one is ink with a signal underline, others muted — radio semantics with arrow keys.

Mono CleanButton

Button

Corporate button: 6px radius, five variants (blue primary, bordered white secondary, ghost, danger, link), three sizes, leading/trailing icons.

Neo CorporateButton

Button Group

Attached segmented button group with shared borders (radio semantics) — for period pickers and view switchers. Selected segment gets a surface-2 fill and bold label.

Neo CorporateButton

Icon Button

Square icon-only button (secondary, ghost, primary, danger) with required aria-label, title tooltip and optional count/dot badge.

Neo CorporateButton

Loading Button

Async action button: idle → spinner + “Processing…” (aria-busy, width locked) → green check “Sent” for 2s → idle. Default simulates a 1.4s payment run.

Neo CorporateButton

Arrow Button

Link-button with a round arrow badge at its end: on hover the badge grows and the arrow turns from ↗ to →, a signature call-to-action for the DA.

Organic SoftButton

Button

Soft pill button: sage primary, sage-tint soft, terracotta clay, outlined ink and ghost variants; three sizes, icons, gentle hover.

Organic SoftButton

Icon Button

Round icon-only button in soft sage, sage, cream (with shadow) or ghost; required label used for aria-label and title.

Organic SoftButton

Segmented

Pill segmented control on a sand track: the selected pill is cream with a soft shadow and slides with a sliding indicator; radio semantics with arrow keys.

Organic SoftButton