---
title: ChatButton
description: An AI-powered chat button with smooth fold/unfold animation, optional offset positioning, and configurable expansion triggers
---

```tsx
'use client';

import { ChatButton } from '@/ui/ChatButton';

export function ChatButtonDemo() {
  return (
    <div className="w-full h-full flex items-center justify-end">
      <ChatButton aria-label="Open Club Med assistant chat" />
    </div>
  );
}
```
```tsx
'use client';

import { ChatButton } from '@/ui/ChatButton';

export function ChatButtonOffsetDemo() {
  return (
    <div className="absolute inset-0 flex items-center justify-end overflow-clip">
      <ChatButton offset aria-label="Open Club Med assistant chat" />
    </div>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/aibutton.json
```

Or manually, copy the following code into `/ui/ChatButton.tsx`:

```tsx
import { Icon } from '@clubmed/trident-icons';
import type { ButtonHTMLAttributes } from 'react';
import { twMerge } from '../helpers/twMerge';

const DEFAULT_TEASER_COPY = 'AI Agent: Have you got a question?';
const FALLBACK_ARIA_LABEL = 'Chat with your assistant';

export type ChatButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
  offset?: boolean;
  unfoldOn?: 'hover' | 'active';
};

export function ChatButton({
  children = DEFAULT_TEASER_COPY,
  className,
  type = 'button',
  'aria-label': ariaLabel,
  tabIndex,
  offset,
  unfoldOn = 'hover',
  ...rest
}: ChatButtonProps) {
  const label =
    ariaLabel ??
    (typeof children === 'string' && children.trim().length > 0
      ? children.trim()
      : FALLBACK_ARIA_LABEL);

  return (
    <button
      type={type}
      aria-label={label}
      tabIndex={tabIndex ?? 0}
      {...rest}
      className={twMerge(
        'chat-border grid grid-cols-[24px_0fr] transition-[grid-template-columns] justify-center items-center overflow-hidden cursor-pointer transition-all rounded-pill px-8 md:px-16 outline-none group/Chatbutton focus-visible:ring-4 focus-visible:ring-black focus-visible:ring-offset-2 pointer-events-auto h-48 min-h-48 md:min-w-80 md:h-80 active:scale-[0.98]',
        offset && 'md:translate-x-[40px] focus-visible:translate-x-0',
        unfoldOn === 'hover' &&
          'group/hoverChatbutton hover:grid-cols-[24px_1fr] hover:translate-x-0',
        unfoldOn === 'active' &&
          'group/activeChatbutton active:grid-cols-[24px_1fr] active:translate-x-0',
        'focus-visible:grid-cols-[24px_1fr]',
        className,
      )}
    >
      <Icon
        name="Sparkles"
        className={twMerge(
          'chat-button-icon w-24 transition-all',
          offset &&
            'relative md:start-[calc(-16px)] group-focus-visible/Chatbutton:start-0 group-hover/hoverChatbutton:start-0 group-active/activeChatbutton:start-0',
        )}
      />
      <div className="grid grid-cols-[0fr] overflow-hidden opacity-0 transition-opacity group-hover/hoverChatbutton:opacity-100 group-focus-visible/Chatbutton:opacity-100 group-active/activeChatbutton:opacity-100 duration-500">
        <span
          aria-hidden="true"
          className="chat-button-text items-center ps-20 flex gap-10 whitespace-nowrap font-semibold text-b3 text-black"
        >
          {children}
          <Icon name="ArrowTailRight" width="24px" />
        </span>
      </div>
    </button>
  );
}
```

## Usage

```tsx showLineNumbers
import { ChatButton } from '@/ui/ChatButton';

export function Component() {
  return <ChatButton>How can I help you today?</ChatButton>;
}
```

## API Reference

### ChatButton

| Prop         | Type                                    | Default                        | Description                                                                      |
| ------------ | --------------------------------------- | ------------------------------ | -------------------------------------------------------------------------------- |
| `children`   | `ReactNode`                             | `"AI Agent: Have you got a question?"`   | The text content displayed in the button                                         |
| `offset`     | `boolean`                               | `false`                        | When true, button is translated right on desktop and slides in on hover/focus. Useful for edge-aligned positioning |
| `unfoldOn`   | `"hover" \| "active"`                   | `"hover"`                      | Controls when the button expands to show text. `"hover"` for discoverability, `"active"` for intentional interaction |
| `className`  | `string`                                | `undefined`                    | Additional CSS classes to merge with default styles                              |
| `type`       | `"button" \| "submit" \| "reset"`       | `"button"`                     | The HTML button type attribute                                                   |
| `aria-label` | `string`                                | Generated from children        | Accessible label for screen readers. Auto-generated from children if not provided|
| `tabIndex`   | `number`                                | `0`                            | The tab order of the button for keyboard navigation                              |
| `disabled`   | `boolean`                               | `false`                        | Whether the button is disabled                                                   |
| `onClick`    | `MouseEventHandler<HTMLButtonElement>` | `undefined`                    | Click event handler                                                              |

The ChatButton also accepts all standard HTML button attributes (`onFocus`, `onBlur`, `onMouseEnter`, `onMouseLeave`, `data-*`, etc.).

## Examples

### Custom Text

<ComponentPreview name="aibutton-custom-text-demo" />

### With Click Handler

<ComponentPreview name="aibutton-custom-handler-demo" />

### Disabled State

<ComponentPreview name="aibutton-disabled-demo" />

## Notes

### Fold/Unfold Animation

The ChatButton features a smooth fold/unfold animation system built using CSS Grid transitions. In the folded state, the button shows only the Sparkles icon. When triggered (by hover, active state, or focus), it expands to reveal the full text content.

**Animation Mechanics:**
- Folded: `grid-cols-[24px_0fr]` - icon visible, text column at 0 fractional units
- Unfolded: `grid-cols-[24px_1fr]` - text column expands to available space
- Text opacity transitions from 0 to 100 with a 500ms duration for smooth reveal
- Transition: `transition-[grid-template-columns]` for smooth grid expansion

### Unfold Trigger Options

The `unfoldOn` prop controls when the button expands:

**`unfoldOn="hover"` (default):**
- Button unfolds on mouse hover
- Most discoverable option for users
- Best for general use cases where you want to encourage interaction

**`unfoldOn="active"` (click/press):**
- Button only unfolds when actively clicked or pressed
- Reduces visual noise from accidental hovers
- Useful in hover-sensitive areas or when you want more intentional interaction

**Focus behavior:** Regardless of the `unfoldOn` setting, the button always unfolds when focused (via keyboard navigation) for accessibility.

### Offset Behavior

The `offset` prop enables a subtle slide-in effect for edge-aligned buttons:

**When `offset={true}`:**
- Desktop only: Button is translated 40px to the right (`md:translate-x-[40px]`)
- Slides back in smoothly on hover or focus (`hover:translate-x-0`, `focus-visible:translate-x-0`)
- Sparkles icon also offsets to maintain visual balance
- Creates a "peek from edge" effect ideal for screen-edge positioning

**Use cases:**
- Buttons positioned at viewport edges (bottom-right corner)
- Creating subtle, non-intrusive chat prompts
- Reducing visual clutter while maintaining discoverability

### Responsive Design

The ChatButton adapts to different screen sizes with breakpoint-based styling:

**Mobile (default):**
- Height: `h-48` (48px)
- Padding: `px-8` (8px horizontal)
- Compact design for touch interfaces

**Desktop (`md:` breakpoint and above):**
- Height: `md:h-80` (80px)
- Padding: `md:px-16` (16px horizontal)
- Minimum width: `md:min-w-80` (80px)
- Larger touch targets and more prominent presence

### Accessibility Features

The component includes several accessibility features:

- **Smart aria-label generation**: If no explicit `aria-label` is provided, the component generates one from the `children` prop. If children is empty or only whitespace, it falls back to "Chat with your assistant"
- **Text span is aria-hidden**: The visible text is hidden from screen readers (aria-hidden="true") to prevent duplicate announcements, since the button already has an aria-label
- **Focus-visible ring**: Shows a black ring (`ring-4 ring-black`) when focused via keyboard
- **Focus triggers unfold**: Button always expands on keyboard focus regardless of `unfoldOn` setting
- **Default tabIndex**: The button is keyboard accessible with a default tabIndex of 0
- **Standard button semantics**: Uses proper `<button>` element with appropriate type attribute

### Positioning

The ChatButton is designed to be positioned as a floating action button, typically in the bottom-right corner of the viewport. Use fixed positioning with appropriate z-index values to ensure it appears above other content. Combine with the `offset` prop for a subtle slide-in effect:

```tsx
<div className="fixed bottom-32 right-32 z-50">
  <ChatButton offset={true}>Need help?</ChatButton>
</div>
```

### Icons

The ChatButton includes two built-in icons:

- **Sparkles icon** (left): Indicates AI-powered functionality, offsets with button when `offset={true}`
- **ArrowTailRight icon** (right): Suggests forward action/interaction

These icons are integral to the component's design and cannot be customized or removed.
