shadimage
wa-ui
componentinteractive-reply-bubblesource: ok · hit

Interactive Reply Bubble

WhatsApp-style interactive reply bubble — shows received button, list, or flow replies with icon and subtype label.

npx shadcn add "https://ui.meta-cloud-api.site/r/interactive-reply-bubble.json"
free

No selection tags yet — empty means no evidence, not lack of support.

Files4 · source cached 10/8/2026, 10:23:08 PM

components/ui/whatsapp/interactive-reply-bubble.tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";
import { type MessageStatus, MessageStatusIcon } from "./message-status";
import { ReplyPreview } from "./reply-preview";
import "@/components/ui/whatsapp/styles/whatsapp.css";

export type InteractiveReplyType =
  | "button_reply"
  | "list_reply"
  | "nfm_reply"
  | "button"
  | string;

export interface InteractiveReplyBubbleProps extends React.HTMLAttributes<HTMLDivElement> {
  variant?: "incoming" | "outgoing";
  /** The reply title or text shown by the user */
  title: string;
  /** Subtype — e.g. "button_reply", "list_reply", "nfm_reply" */
  replyType?: InteractiveReplyType;
  /** Optional description (for list replies) */
  description?: string;
  /** Author of the quoted message, e.g. "You" */
  quotedAuthor?: string;
  /** Accent color for the quote bar and author name */
  quotedAuthorColor?: string;
  /** Body of the message being replied to */
  quotedText?: string;
  timestamp?: string;
  status?: MessageStatus;
  showTail?: boolean;
}

/**
 * A button/list reply as WhatsApp renders it: a quote of the message the
 * user tapped, followed by the chosen option as plain text.
 */
const InteractiveReplyBubble = React.forwardRef<HTMLDivElement, InteractiveReplyBubbleProps>(
  (
    {
      className,
      variant = "incoming",
      title,
      replyType,
      description,
      quotedAuthor,
      quotedAuthorColor,
      quotedText,
      timestamp,
      status,
      showTail = false,
      ...props
    },
    ref
  ) => {
    const isOutgoing = variant === "outgoing";

    return (
      <div
        className={cn(
          "flex w-full",
          isOutgoing ? "justify-end" : "justify-start",
          showTail ? "mb-[6px]" : "mb-[2px]",
          className
        )}
        data-reply-type={replyType}
        {...props}
        ref={ref}
      >
        <div
          className={cn(
            "font-wa relative max-w-[60%] overflow-visible rounded-lg px-3 pb-[7px] pt-[6px]",
            quotedText && "min-w-[180px] px-[6px] pt-[6px]",
            isOutgoing ? "bg-wa-bubble-outgoing" : "bg-wa-bubble-incoming",
            showTail && (isOutgoing ? "rounded-br-none" : "rounded-bl-none")
          )}
        >
          {showTail && isOutgoing && (
            <svg viewBox="0 0 8 13" width="8" height="13" className="absolute bottom-0 -right-[8px]">
              <path opacity="0.13" d="M5.188 12H0V0.807l6.467 8.625C7.526 10.844 6.958 12 5.188 12z" className="fill-wa-always-black" />
              <path d="M5.188 13H0V1.807l6.467 8.625C7.526 11.844 6.958 13 5.188 13z" className="fill-wa-bubble-outgoing" />
            </svg>
          )}
          {showTail && !isOutgoing && (
            <svg viewBox="0 0 8 13" width="8" height="13" className="absolute bottom-0 -left-[8px]">
              <path opacity="0.13" d="M2.812 12H8V0.807L1.533 9.432C0.474 10.844 1.042 12 2.812 12z" className="fill-wa-always-black" />
              <path d="M2.812 13H8V1.807L1.533 10.432C0.474 11.844 1.042 13 2.812 13z" className="fill-wa-bubble-incoming" />
            </svg>
          )}

          {quotedText && (
            <ReplyPreview
              className="mb-[4px]"
              author={quotedAuthor || "You"}
              authorColor={quotedAuthorColor}
              body={quotedText}
            />
          )}

          <div className={cn(quotedText && "px-[6px]")}>
            <p className="whitespace-pre-wrap break-words text-[14.2px] leading-[19px] text-wa-text-primary">
              {title}
            </p>

            {description && (
              <p className="mt-[2px] text-[12.5px] leading-[17px] text-wa-text-secondary">
                {description}
              </p>
            )}

            <div className="mt-[2px] flex items-center justify-end gap-[3px]">
              {timestamp && (
                <span className="text-[11px] leading-[15px] text-wa-bubble-meta">
                  {timestamp}
                </span>
              )}
              {isOutgoing && status && <MessageStatusIcon status={status} />}
            </div>
          </div>
        </div>
      </div>
    );
  }
);
InteractiveReplyBubble.displayName = "InteractiveReplyBubble";

export { InteractiveReplyBubble };
components/ui/whatsapp/reply-preview.tsx
"use client";

import * as React from "react";
import { cn } from "@/lib/utils";
import "@/components/ui/whatsapp/styles/whatsapp.css";

type MediaType = "text" | "image" | "video" | "audio" | "document" | "sticker" | "location" | "contact";

const MEDIA_ICONS: Record<Exclude<MediaType, "text">, React.ReactNode> = {
  image:    <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M21 19V5c0-1.1-.9-2-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2zM8.5 13.5l2.5 3L14.5 12l4.5 6H5l3.5-4.5z"/></svg>,
  video:    <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M17 10.5V7c0-.55-.45-1-1-1H4c-.55 0-1 .45-1 1v10c0 .55.45 1 1 1h12c.55 0 1-.45 1-1v-3.5l4 4v-11l-4 4z"/></svg>,
  audio:    <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M12 3a9 9 0 1 0 9 9c0-.46-.04-.92-.1-1.36a5.4 5.4 0 0 1-4.4 2.26 5.4 5.4 0 0 1-3.14-9.8c-.44-.06-.9-.1-1.36-.1zm-1 7v4l3-2-3-2z"/></svg>,
  document: <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm-1 7V3.5L18.5 9H13z"/></svg>,
  sticker:  <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M11.5 2C6.81 2 3 5.81 3 10.5S6.81 19 11.5 19h.5v3c4.86-2.34 8-7 8-11.5C20 5.81 16.19 2 11.5 2zm1 10.5h-3v-3h3v3zm0-4.5h-3V6h3v2z"/></svg>,
  location: <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>,
  contact:  <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>,
};

export interface ReplyPreviewProps extends React.HTMLAttributes<HTMLDivElement> {
  author?: string;
  authorColor?: string;
  body?: string;
  mediaType?: MediaType;
  mediaThumb?: string;
  onClick?: () => void;
}

/**
 * Quote/reply preview strip shown inside a bubble.
 * Place this as the first child inside a ChatBubble or any message bubble.
 */
const ReplyPreview = React.forwardRef<HTMLDivElement, ReplyPreviewProps>(
  (
    {
      className,
      author,
      authorColor = "var(--wa-emerald-500)",
      body,
      mediaType = "text",
      mediaThumb,
      onClick,
      ...props
    },
    ref
  ) => {
    return (
      <div
        ref={ref}
        role={onClick ? "button" : undefined}
        tabIndex={onClick ? 0 : undefined}
        onClick={onClick}
        onKeyDown={onClick ? (e) => e.key === "Enter" && onClick() : undefined}
        className={cn(
          "mb-[6px] flex min-h-[42px] overflow-hidden rounded-[4px]",
          "bg-black/[0.05] dark:bg-white/[0.05]",
          onClick && "cursor-pointer hover:bg-black/[0.08] dark:hover:bg-white/[0.08]",
          className
        )}
        {...props}
      >
        {/* Accent bar */}
        <div className="w-[3px] shrink-0" style={{ backgroundColor: authorColor }} />

        {/* Content */}
        <div className="flex min-w-0 flex-1 flex-col justify-center px-[8px] py-[5px]">
          {author && (
            <p className="text-[12.5px] font-semibold leading-[17px]" style={{ color: authorColor }}>
              {author}
            </p>
          )}
          <div className="flex items-center gap-1">
            {mediaType !== "text" && (
              <span className="shrink-0 text-wa-text-secondary">{MEDIA_ICONS[mediaType]}</span>
            )}
            {body && (
              <p className="line-clamp-2 text-[12.5px] leading-[17px] text-wa-text-secondary">
                {body}
              </p>
            )}
          </div>
        </div>

        {/* Media thumbnail */}
        {mediaThumb && (
          <div className="h-[52px] w-[52px] shrink-0 overflow-hidden">
            <img src={mediaThumb} alt="" className="h-full w-full object-cover" />
          </div>
        )}
      </div>
    );
  }
);
ReplyPreview.displayName = "ReplyPreview";

export { ReplyPreview };
components/ui/whatsapp/message-status.tsx
"use client";

import * as React from "react";
import "@/components/ui/whatsapp/styles/whatsapp.css";

export type MessageStatus = "sending" | "sent" | "delivered" | "read" | "failed";

function CheckIcon({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 16 11" height="11" width="16" className={className}>
      <path d="M11.071.653a.457.457 0 0 0-.304-.102.493.493 0 0 0-.381.178l-6.19 7.636-2.011-2.175a.463.463 0 0 0-.349-.158.467.467 0 0 0-.338.131.537.537 0 0 0-.14.353.54.54 0 0 0 .13.363l2.39 2.576a.465.465 0 0 0 .349.158.457.457 0 0 0 .368-.189l6.596-8.14a.504.504 0 0 0 .103-.36.516.516 0 0 0-.223-.271z" fill="currentColor" />
    </svg>
  );
}

function DoubleCheckIcon({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 16 11" height="11" width="16" className={className}>
      <path d="M11.071.653a.457.457 0 0 0-.304-.102.493.493 0 0 0-.381.178l-6.19 7.636-2.011-2.175a.463.463 0 0 0-.349-.158.467.467 0 0 0-.338.131.537.537 0 0 0-.14.353.54.54 0 0 0 .13.363l2.39 2.576a.465.465 0 0 0 .349.158.457.457 0 0 0 .368-.189l6.596-8.14a.504.504 0 0 0 .103-.36.516.516 0 0 0-.223-.271z" fill="currentColor" />
      <path d="M15.071.653a.457.457 0 0 0-.304-.102.493.493 0 0 0-.381.178l-6.19 7.636-1.2-1.298-.728.897 1.58 1.704a.465.465 0 0 0 .349.158.457.457 0 0 0 .368-.189l6.596-8.14a.504.504 0 0 0 .103-.36.516.516 0 0 0-.193-.484z" fill="currentColor" />
    </svg>
  );
}

/** Renders the appropriate status icon for a given MessageStatus. */
export function MessageStatusIcon({ status }: { status: MessageStatus }) {
  switch (status) {
    case "sending":
    case "sent":
      return <span className="text-wa-delivered opacity-75"><CheckIcon /></span>;
    case "delivered":
      return <span className="text-wa-delivered"><DoubleCheckIcon /></span>;
    case "read":
      return <span className="text-wa-read"><DoubleCheckIcon /></span>;
    case "failed":
      return (
        <span className="text-wa-text-critical">
          <svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor">
            <path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z" />
          </svg>
        </span>
      );
  }
}
components/ui/whatsapp/styles/whatsapp.css
/*
 * WhatsApp UI Design Tokens
 * Extracted from web.whatsapp.com WDS (WhatsApp Design System)
 */

@theme inline {
  /* === Primary Brand Colors === */
  --color-wa-green-50: var(--wa-green-50);
  --color-wa-green-75: var(--wa-green-75);
  --color-wa-green-100: var(--wa-green-100);
  --color-wa-green-200: var(--wa-green-200);
  --color-wa-green-300: var(--wa-green-300);
  --color-wa-green-400: var(--wa-green-400);
  --color-wa-green-450: var(--wa-green-450);
  --color-wa-green-500: var(--wa-green-500);
  --color-wa-green-600: var(--wa-green-600);
  --color-wa-green-700: var(--wa-green-700);
  --color-wa-green-750: var(--wa-green-750);
  --color-wa-green-800: var(--wa-green-800);

  /* === Emerald (Teal) === */
  --color-wa-emerald-400: var(--wa-emerald-400);
  --color-wa-emerald-500: var(--wa-emerald-500);
  --color-wa-emerald-600: var(--wa-emerald-600);

  /* === Neutral Gray === */
  --color-wa-gray-50: var(--wa-gray-50);
  --color-wa-gray-75: var(--wa-gray-75);
  --color-wa-gray-100: var(--wa-gray-100);
  --color-wa-gray-200: var(--wa-gray-200);
  --color-wa-gray-300: var(--wa-gray-300);
  --color-wa-gray-400: var(--wa-gray-400);
  --color-wa-gray-500: var(--wa-gray-500);
  --color-wa-gray-600: var(--wa-gray-600);
  --color-wa-gray-700: var(--wa-gray-700);
  --color-wa-gray-800: var(--wa-gray-800);
  --color-wa-gray-850: var(--wa-gray-850);
  --color-wa-gray-900: var(--wa-gray-900);
  --color-wa-gray-1000: var(--wa-gray-1000);

  /* === Sky Blue (Read Receipts) === */
  --color-wa-sky-blue-300: var(--wa-sky-blue-300);
  --color-wa-sky-blue-400: var(--wa-sky-blue-400);

  /* === Red (Destructive/Negative) === */
  --color-wa-red-300: var(--wa-red-300);
  --color-wa-red-400: var(--wa-red-400);

  /* === Cobalt === */
  --color-wa-cobalt-300: var(--wa-cobalt-300);
  --color-wa-cobalt-400: var(--wa-cobalt-400);

  /* === Persistent/Brand === */
  --color-wa-verified: var(--wa-verified);
  --color-wa-always-white: var(--wa-always-white);
  --color-wa-always-black: var(--wa-always-black);

  /* === Profile Photo Avatars === */
  --color-wa-avatar-brown: var(--wa-avatar-brown);
  --color-wa-avatar-cobalt: var(--wa-avatar-cobalt);
  --color-wa-avatar-green: var(--wa-avatar-green);
  --color-wa-avatar-orange: var(--wa-avatar-orange);
  --color-wa-avatar-pink: var(--wa-avatar-pink);
  --color-wa-avatar-purple: var(--wa-avatar-purple);
  --color-wa-avatar-red: var(--wa-avatar-red);
  --color-wa-avatar-sky-blue: var(--wa-avatar-sky-blue);
  --color-wa-avatar-teal: var(--wa-avatar-teal);
  --color-wa-avatar-yellow: var(--wa-avatar-yellow);

  /* === Semantic Colors (theme-aware) === */
  --color-wa-bg: var(--wa-bg);
  --color-wa-bg-deeper: var(--wa-bg-deeper);
  --color-wa-bg-wash: var(--wa-bg-wash);
  --color-wa-panel-bg: var(--wa-panel-bg);
  --color-wa-panel-header-bg: var(--wa-panel-header-bg);
  --color-wa-conversation-bg: var(--wa-conversation-bg);
  --color-wa-compose-bg: var(--wa-compose-bg);

  --color-wa-text-primary: var(--wa-text-primary);
  --color-wa-text-secondary: var(--wa-text-secondary);
  --color-wa-text-deemphasized: var(--wa-text-deemphasized);

  --color-wa-bubble-outgoing: var(--wa-bubble-outgoing);
  --color-wa-bubble-incoming: var(--wa-bubble-incoming);
  --color-wa-bubble-system: var(--wa-bubble-system);
  --color-wa-bubble-meta: var(--wa-bubble-meta);

  --color-wa-border: var(--wa-border);
  --color-wa-divider: var(--wa-divider);

  --color-wa-primary: var(--wa-primary);
  --color-wa-primary-hover: var(--wa-primary-hover);
  --color-wa-icon-default: var(--wa-icon-default);
  --color-wa-icon-lighter: var(--wa-icon-lighter);
  --color-wa-hover: var(--wa-hover);
  --color-wa-active: var(--wa-active);

  --color-wa-read: var(--wa-read);
  --color-wa-delivered: var(--wa-delivered);

  --color-wa-unread-marker: var(--wa-unread-marker);
  --color-wa-typing-dot: var(--wa-typing-dot);
  --color-wa-input-bg: var(--wa-input-bg);
  --color-wa-input-border: var(--wa-input-border);

  --color-wa-wallpaper-bg: var(--wa-wallpaper-bg);
  --color-wa-wallpaper-fg: var(--wa-wallpaper-fg);

  /* === Additional Color Scales === */
  --color-wa-teal-300: var(--wa-teal-300);
  --color-wa-teal-400: var(--wa-teal-400);
  --color-wa-teal-500: var(--wa-teal-500);

  --color-wa-purple-300: var(--wa-purple-300);
  --color-wa-purple-400: var(--wa-purple-400);

  --color-wa-orange-300: var(--wa-orange-300);
  --color-wa-orange-400: var(--wa-orange-400);

  --color-wa-yellow-300: var(--wa-yellow-300);
  --color-wa-yellow-400: var(--wa-yellow-400);

  --color-wa-pink-300: var(--wa-pink-300);
  --color-wa-pink-400: var(--wa-pink-400);

  /* === Attachment Type Colors === */
  --color-wa-attachment-document: var(--wa-attachment-document);
  --color-wa-attachment-audio: var(--wa-attachment-audio);
  --color-wa-attachment-photo: var(--wa-attachment-photo);
  --color-wa-attachment-contact: var(--wa-attachment-contact);
  --color-wa-attachment-camera: var(--wa-attachment-camera);
  --color-wa-attachment-sticker: var(--wa-attachment-sticker);
  --color-wa-attachment-poll: var(--wa-attachment-poll);
  --color-wa-attachment-catalog: var(--wa-attachment-catalog);

  /* === Semantic Text === */
  --color-wa-text-disabled: var(--wa-text-disabled);
  --color-wa-text-critical: var(--wa-text-critical);
  --color-wa-text-success: var(--wa-text-success);
  --color-wa-text-action: var(--wa-text-action);
  --color-wa-system-msg: var(--wa-system-msg);
  --color-wa-primary-title: var(--wa-primary-title);

  /* === Typography === */
  --font-wa: "Segoe UI", "Helvetica Neue", Helvetica, "Lucida Grande", Arial,
    Ubuntu, Cantarell, "Fira Sans", sans-serif;

  /* === Layout Constants === */
  --spacing-wa-sidebar-width: var(--wa-sidebar-width);
  --spacing-wa-sidebar-min-width: var(--wa-sidebar-min-width);
  --spacing-wa-header-height: var(--wa-header-height);
  --spacing-wa-compose-height: var(--wa-compose-height);
  --spacing-wa-chat-spacing: var(--wa-chat-spacing);
  --spacing-wa-avatar-sm: var(--wa-avatar-size-sm);
  --spacing-wa-avatar-md: var(--wa-avatar-size-md);
  --spacing-wa-avatar-lg: var(--wa-avatar-size-lg);

  /* === Border Radius === */
  --radius-wa-bubble: var(--wa-bubble-radius);

  /* === Animation === */
  --animate-wa-typing-bounce: wa-typing-bounce 1.2s ease-in-out infinite;
}

@keyframes wa-typing-bounce {
  0%,
  60%,
  100% {
    transform: translateY(0);
  }
  30% {
    transform: translateY(-4px);
  }
}

:root {
  /* === Primary Brand Colors === */
  --wa-green-50: #f2fdf0;
  --wa-green-75: #e7fce3;
  --wa-green-100: #d9fdd3;
  --wa-green-200: #acfcac;
  --wa-green-300: #71eb85;
  --wa-green-400: #25d366;
  --wa-green-450: #21c063;
  --wa-green-500: #1daa61;
  --wa-green-600: #1b8755;
  --wa-green-700: #15603e;
  --wa-green-750: #144d37;
  --wa-green-800: #103529;

  /* === Emerald (Teal) === */
  --wa-emerald-400: #06cf9c;
  --wa-emerald-500: #00a884;
  --wa-emerald-600: #008069;

  /* === Neutral Gray === */
  --wa-gray-50: #fafafa;
  --wa-gray-75: #f4f4f4;
  --wa-gray-100: #eeeeee;
  --wa-gray-200: #d8d8d8;
  --wa-gray-300: #bdbdbd;
  --wa-gray-400: #959393;
  --wa-gray-500: #757778;
  --wa-gray-600: #606263;
  --wa-gray-700: #424445;
  --wa-gray-800: #242626;
  --wa-gray-850: #1d1f1f;
  --wa-gray-900: #161717;
  --wa-gray-1000: #0a0a0a;

  /* === Sky Blue (Read Receipts) === */
  --wa-sky-blue-300: #53bdeb;
  --wa-sky-blue-400: #009de2;

  /* === Red (Destructive/Negative) === */
  --wa-red-300: #fb5061;
  --wa-red-400: #f53947;

  /* === Cobalt === */
  --wa-cobalt-300: #53a6fd;
  --wa-cobalt-400: #007bfc;

  /* === Persistent/Brand === */
  --wa-verified: #0085f4;
  --wa-always-white: #ffffff;
  --wa-always-black: #000000;

  /* === Profile Photo Avatars === */
  --wa-avatar-brown: #dba685;
  --wa-avatar-cobalt: #53a6fd;
  --wa-avatar-green: #25d366;
  --wa-avatar-orange: #fc9775;
  --wa-avatar-pink: #ff72a1;
  --wa-avatar-purple: #a791ff;
  --wa-avatar-red: #fb5061;
  --wa-avatar-sky-blue: #53bdeb;
  --wa-avatar-teal: #42c7b8;
  --wa-avatar-yellow: #ffd279;
}

/* === Light Mode (WhatsApp Default) === */
:root,
[data-theme="light"] {
  /* Surfaces */
  --wa-bg: #ffffff;
  --wa-bg-deeper: #f0f2f5;
  --wa-bg-wash: #f7f8fa;
  --wa-panel-bg: #ffffff;
  --wa-panel-header-bg: #ffffff;
  --wa-conversation-bg: #f5f0e8;
  --wa-compose-bg: #f0f2f5;

  /* Content */
  --wa-text-primary: #111b21;
  --wa-text-secondary: #667781;
  --wa-text-deemphasized: rgba(17, 27, 33, 0.4);

  /* Chat Bubbles */
  --wa-bubble-outgoing: #e7ffdb;
  --wa-bubble-incoming: #ffffff;
  --wa-bubble-system: #ffecd2;
  --wa-bubble-radius: 7.5px;
  --wa-bubble-meta: rgba(17, 27, 33, 0.45);

  /* Lines */
  --wa-border: #e9edef;
  --wa-divider: rgba(134, 150, 160, 0.15);

  /* Interactive */
  --wa-primary: #008069;
  --wa-primary-hover: #017561;
  --wa-icon-default: #54656f;
  --wa-icon-lighter: #8696a0;
  --wa-hover: rgba(17, 27, 33, 0.05);
  --wa-active: rgba(17, 27, 33, 0.1);

  /* Read Receipts */
  --wa-read: #53bdeb;
  --wa-delivered: #8696a0;

  /* Misc */
  --wa-unread-marker: #25d366;
  --wa-typing-dot: #8696a0;
  --wa-input-bg: #ffffff;
  --wa-input-border: #e4e6eb;

  /* Chat Wallpaper */
  --wa-wallpaper-bg: #f5f0e8;
  --wa-wallpaper-fg: rgba(17, 27, 33, 0.05);
  --wa-wallpaper-image: url("/wa-wallpaper.png");
  --wa-wallpaper-tint: rgba(229, 221, 213, 0.85);

  /* Template header placeholder */
  --wa-placeholder-bg: #ccd0d5;
  --wa-placeholder-icon: brightness(0) saturate(100%) invert(42%) sepia(9%) saturate(621%) hue-rotate(155deg) brightness(94%) contrast(87%);

  /* Template button icon — converts PNG to wa-emerald-500 (#00a884) */
  --wa-btn-icon-filter: brightness(0) saturate(100%) invert(50%) sepia(97%) saturate(372%) hue-rotate(128deg) brightness(97%);

  /* === Additional Color Scales === */
  --wa-teal-300: #42c7b8;
  --wa-teal-400: #02a698;
  --wa-teal-500: #028377;

  --wa-purple-300: #a791ff;
  --wa-purple-400: #7f66ff;

  --wa-orange-300: #fc9775;
  --wa-orange-400: #fa6533;

  --wa-yellow-300: #ffd279;
  --wa-yellow-400: #ffb938;

  --wa-pink-300: #ff72a1;
  --wa-pink-400: #ff2e74;

  /* === Attachment Type Colors === */
  --wa-attachment-document: #7f66ff;
  --wa-attachment-audio: #fa6533;
  --wa-attachment-photo: #007bfc;
  --wa-attachment-contact: #009de2;
  --wa-attachment-camera: #ff2e74;
  --wa-attachment-sticker: #02a698;
  --wa-attachment-poll: #ffbc38;
  --wa-attachment-catalog: #3a5564;

  /* === Semantic Text === */
  --wa-text-disabled: #d1d7db;
  --wa-text-critical: #ea0038;
  --wa-text-success: #1fa855;
  --wa-text-action: #008069;
  --wa-system-msg: #54656f;
  --wa-primary-title: #41525d;

  /* === Shadows === */
  --wa-shadow-light: rgba(11, 20, 26, .08);
  --wa-shadow-medium: rgba(11, 20, 26, .15);
  --wa-border-bubble: rgba(17, 27, 33, .06);

  /* === Button States === */
  --wa-button-primary-hover: #017561;
  --wa-filter-text: #54656f;
  --wa-filter-active: #008069;
  --wa-filter-bg-active: #d9fdd3;

  /* === Spacing === */
  --wa-text-size-small: 12.6px;
  --wa-bubble-padding-start: 9px;
  --wa-bubble-padding-end: 7px;
}

/* === Dark Mode === */
[data-theme="dark"],
.dark {
  /* Surfaces */
  --wa-bg: #111b21;
  --wa-bg-deeper: #0b141a;
  --wa-bg-wash: #161717;
  --wa-panel-bg: #111b21;
  --wa-panel-header-bg: #202c33;
  --wa-conversation-bg: #0b141a;
  --wa-compose-bg: #202c33;

  /* Content */
  --wa-text-primary: #e9edef;
  --wa-text-secondary: #8696a0;
  --wa-text-deemphasized: rgba(233, 237, 239, 0.4);

  /* Chat Bubbles */
  --wa-bubble-outgoing: #005c4b;
  --wa-bubble-incoming: #202c33;
  --wa-bubble-system: #182229;
  --wa-bubble-meta: rgba(255, 255, 255, 0.6);

  /* Lines */
  --wa-border: rgba(233, 237, 239, 0.12);
  --wa-divider: rgba(134, 150, 160, 0.15);

  /* Interactive */
  --wa-primary: #00a884;
  --wa-primary-hover: #06cf9c;
  --wa-icon-default: #aebac1;
  --wa-icon-lighter: #8696a0;
  --wa-hover: rgba(233, 237, 239, 0.05);
  --wa-active: rgba(233, 237, 239, 0.1);

  /* Read Receipts */
  --wa-read: #53bdeb;
  --wa-delivered: #aebac1;

  /* Misc */
  --wa-unread-marker: #00a884;
  --wa-typing-dot: #8696a0;
  --wa-input-bg: #2a3942;
  --wa-input-border: rgba(233, 237, 239, 0.12);

  /* Chat Wallpaper */
  --wa-wallpaper-bg: #0b141a;
  --wa-wallpaper-fg: rgba(255, 255, 255, 0.06);
  --wa-wallpaper-image: url("/wa-wallpaper.png");
  --wa-wallpaper-tint: rgba(11, 20, 26, 0.88);

  /* Template header placeholder */
  --wa-placeholder-bg: #2a3942;
  --wa-placeholder-icon: brightness(0) saturate(100%) invert(62%) sepia(10%) saturate(400%) hue-rotate(155deg) brightness(95%) contrast(85%);

  /* Template button icon — converts PNG to wa-emerald-400 (#06cf9c) */
  --wa-btn-icon-filter: brightness(0) saturate(100%) invert(68%) sepia(60%) saturate(400%) hue-rotate(118deg) brightness(100%);

  /* === Semantic Text (dark) === */
  --wa-text-disabled: #3b4a54;
  --wa-text-critical: #ff6b7a;
  --wa-text-success: #25d366;
  --wa-text-action: #00a884;
  --wa-system-msg: #8696a0;
  --wa-primary-title: #8696a0;

  /* === Shadows (dark) === */
  --wa-shadow-light: rgba(0, 0, 0, .3);
  --wa-shadow-medium: rgba(0, 0, 0, .4);
  --wa-border-bubble: rgba(255, 255, 255, .06);

  /* === Button States (dark) === */
  --wa-button-primary-hover: #06cf9c;
  --wa-filter-text: #8696a0;
  --wa-filter-active: #00a884;
  --wa-filter-bg-active: #005c4b;
}

/* === Wallpaper === */
.wa-wallpaper {
  position: relative;
  background-color: var(--wa-wallpaper-bg);
  background-image: var(--wa-wallpaper-image);
  background-repeat: repeat;
  background-size: 412px auto;
  background-blend-mode: overlay;
}

.dark .wa-wallpaper,
[data-theme="dark"] .wa-wallpaper {
  background-blend-mode: soft-light;
}

/* === Layout Constants === */
:root {
  --wa-sidebar-width: 440px;
  --wa-sidebar-min-width: 340px;
  --wa-header-height: 59px;
  --wa-compose-height: 62px;
  --wa-chat-spacing: 15px;
  --wa-avatar-size-sm: 40px;
  --wa-avatar-size-md: 49px;
  --wa-avatar-size-lg: 200px;
  --wa-msg-max-width: 60%;
}

More from wa-ui

Browse all components in this library →