---
title: Chip
description: An inline, text-flowing token — used for mentions in the composer and reconstructed chips in messages.
source: chip
---

```tsx title="primitives/chip/demos/basic.tsx"
"use client";

import { Chip } from "@intentface/chat/chip";
import { IconWorld } from "@tabler/icons-react";
import type { ReactElement, ReactNode } from "react";

// Chips flow inline with text. `variant` is an opaque string surfaced as
// data-variant, so the tinting rules are entirely yours.
export const Basic = () => (
  <p className="max-w-md text-sm leading-8 text-[#1a1a1a] dark:text-[#fcfcfc]">
    Pulled results from{" "}
    <Chip.Root variant="accent" className={CHIP_CLASS}>
      <Chip.Icon className="flex items-center">
        <IconWorld className="size-4" />
      </Chip.Icon>
      <Chip.Label>web-search</Chip.Label>
    </Chip.Root>{" "}
    and a{" "}
    <Chip.Root className={CHIP_CLASS} renderWithPreview={renderWithPreview}>
      <Chip.Label>document</Chip.Label>
      <Chip.Preview>Hover shows a preview panel for the referenced item.</Chip.Preview>
    </Chip.Root>{" "}
    reference, with one{" "}
    <Chip.Root variant="warning" className={CHIP_CLASS}>
      <Chip.Label>deprecated</Chip.Label>
    </Chip.Root>{" "}
    flag.
  </p>
);

const CHIP_CLASS =
  "mx-0.5 inline-flex items-center gap-1 rounded-md border border-[#f0f0f0] bg-[#f4f4f4] px-1.5 py-0.5 align-baseline text-xs font-medium data-[variant=accent]:border-blue-200 data-[variant=accent]:bg-blue-50 data-[variant=accent]:text-blue-700 data-[variant=warning]:border-amber-200 data-[variant=warning]:bg-amber-50 data-[variant=warning]:text-amber-700 dark:border-[#2d2d2d] dark:bg-[#232323] dark:data-[variant=accent]:border-blue-900 dark:data-[variant=accent]:bg-blue-950 dark:data-[variant=accent]:text-blue-300 dark:data-[variant=warning]:border-amber-900 dark:data-[variant=warning]:bg-amber-950 dark:data-[variant=warning]:text-amber-300";

// Chip.Preview renders nothing on its own — Root hands you the badge and the
// preview content, and you compose whatever popup you want. This one is pure
// CSS so the demo needs no floating library.
const renderWithPreview = (badge: ReactElement, preview: ReactNode) => (
  <span className="group relative inline-block">
    {badge}
    <span className="pointer-events-none absolute bottom-full left-1/2 z-10 mb-1 w-52 -translate-x-1/2 rounded-lg border border-[#f0f0f0] bg-white p-2 text-xs leading-snug text-[#686868] opacity-0 shadow-md transition-opacity group-hover:opacity-100 dark:border-[#2d2d2d] dark:bg-[#181818] dark:text-[#9b9b9b]">
      {preview}
    </span>
  </span>
);
```

## Usage guidelines

- **Inline token** — flows with the surrounding text instead of breaking the line box.
- **Two homes** — backs the composer's mention decorations and the chips a message reconstructs from its wire format.
- **Variants** — `primary` / `accent` / `warning` tint the surface.
- **Hover preview** — a `Chip.Preview` child promotes the chip to a hover card and is never rendered inline.
- **Get started** — see [Quick start](/quick-start) to add the package.

## Anatomy

```tsx
<Chip.Root variant="accent">
  <Chip.Icon>{icon}</Chip.Icon>
  <Chip.Label>{label}</Chip.Label>
</Chip.Root>
```

With a hover preview — the `Chip.Preview` child is lifted into a hover card and
never rendered inline:

```tsx
<Chip.Root>
  <Chip.Label>{label}</Chip.Label>
  <Chip.Preview>
    <SourceCard source={source} />
  </Chip.Preview>
</Chip.Root>
```

## Examples

### Building the preview surface

`Chip.Preview` renders nothing itself. `renderWithPreview` hands you the badge
and the preview content and lets you decide what surface they go in, which is
what keeps a floating-UI dependency out of the package.

Whatever you build there inherits the hover-card obligations: it opens on
keyboard focus as well as hover, and Escape dismisses it. A CSS-only `:hover`
panel looks right and cannot be reached without a pointer.

```tsx title="primitives/chip/demos/preview.tsx"
"use client";

import { Chip } from "@intentface/chat/chip";
import { type ReactElement, type ReactNode, useEffect, useRef, useState } from "react";

/*
 * `Chip.Preview` renders nothing itself. `renderWithPreview` hands you the
 * badge and the preview content and lets you decide what surface they go in,
 * which is the seam that keeps the package free of a floating library.
 *
 * Whatever you build there inherits the hover-card obligations: it has to open
 * on keyboard focus as well as hover, and Escape has to dismiss it. A CSS-only
 * `:hover` panel looks right and is unreachable without a pointer.
 */
export const Preview = () => (
  <p className="max-w-md text-[#1a1a1a] text-sm leading-8 dark:text-[#fcfcfc]">
    Cited{" "}
    <Chip.Root className={chipClass} renderWithPreview={renderWithPreview}>
      <Chip.Label>rfc-1149</Chip.Label>
      <Chip.Preview>
        <span className="font-medium text-[#1a1a1a] dark:text-[#fcfcfc]">
          A Standard for the Transmission of IP Datagrams on Avian Carriers
        </span>
        <span className="mt-1 block">Network Working Group, April 1990.</span>
      </Chip.Preview>
    </Chip.Root>{" "}
    and{" "}
    <Chip.Root variant="accent" className={chipClass} renderWithPreview={renderWithPreview}>
      <Chip.Label>rfc-2324</Chip.Label>
      <Chip.Preview>
        <span className="font-medium text-[#1a1a1a] dark:text-[#fcfcfc]">
          Hyper Text Coffee Pot Control Protocol
        </span>
        <span className="mt-1 block">Network Working Group, April 1998.</span>
      </Chip.Preview>
    </Chip.Root>
    . Tab to a chip, or hover it.
  </p>
);

/**
 * Hover and focus both open it, Escape and blur both close it, and the panel is
 * `aria-hidden` while closed so it never reaches a screen reader out of turn.
 * A real app would reach for a positioned hover card instead of hand-rolling
 * this; the obligations are the same either way.
 */
const renderWithPreview = (badge: ReactElement, preview: ReactNode) => (
  <PreviewSurface badge={badge} preview={preview} />
);

const PreviewSurface = ({ badge, preview }: { badge: ReactElement; preview: ReactNode }) => {
  const [open, setOpen] = useState(false);
  const host = useRef<HTMLSpanElement>(null);

  useEffect(() => {
    if (!open) return;
    const onKeyDown = (event: KeyboardEvent) => {
      if (event.key === "Escape") setOpen(false);
    };
    document.addEventListener("keydown", onKeyDown);
    return () => document.removeEventListener("keydown", onKeyDown);
  }, [open]);

  return (
    <span ref={host} className="relative inline-block">
      {/* A real button, not a span with a tabIndex: this is the control that
          discloses the preview, so it should be one. It carries the pointer and
          focus handlers too, which keeps every listener on an element that can
          actually receive them. */}
      <button
        type="button"
        onPointerEnter={() => setOpen(true)}
        onPointerLeave={() => setOpen(false)}
        onFocus={() => setOpen(true)}
        onBlur={() => setOpen(false)}
        className="cursor-pointer rounded-md align-baseline focus-visible:outline-2 focus-visible:outline-[#1a1a1a] focus-visible:outline-offset-2 dark:focus-visible:outline-[#fcfcfc]"
      >
        {badge}
      </button>

      <span
        aria-hidden={!open}
        className={[
          "pointer-events-none absolute bottom-full left-1/2 z-10 mb-1.5 w-56 -translate-x-1/2",
          "rounded-lg border border-[#f0f0f0] bg-white p-2.5 text-left text-[#686868] text-xs leading-snug shadow-md",
          "transition-opacity duration-150 dark:border-[#2d2d2d] dark:bg-[#181818] dark:text-[#9b9b9b]",
          open ? "opacity-100" : "opacity-0",
        ].join(" ")}
      >
        {preview}
      </span>
    </span>
  );
};

const chipClass =
  "mx-0.5 inline-flex items-center gap-1 rounded-md border border-[#f0f0f0] bg-[#f4f4f4] px-1.5 py-0.5 align-baseline font-medium text-xs data-[variant=accent]:border-blue-200 data-[variant=accent]:bg-blue-50 data-[variant=accent]:text-blue-700 dark:border-[#2d2d2d] dark:bg-[#232323] dark:data-[variant=accent]:border-blue-900 dark:data-[variant=accent]:bg-blue-950 dark:data-[variant=accent]:text-blue-300";
```

## API reference

Every part accepts `className`, `style`, and `render` (see
[Styling](/handbook/styling)) and emits a bespoke part attribute (`data-<part>`) unless noted.

### Chip

The inline token surface. Flows with the surrounding text rather than breaking
the line box, and lifts a `Chip.Preview` child into whatever surface
`renderWithPreview` builds. Renders a `<span>` element.

export const rootProps = [
  { name: "variant", type: "string", default: "undefined", description: "An opaque styling hook, surfaced as data-variant. The package takes no view on the values; the demo uses primary, accent and warning." },
  { name: "renderWithPreview", type: "(badge: ReactElement, preview: ReactNode) => ReactNode", description: "Called when a Chip.Preview child is present. Receives the rendered badge and the preview content; compose your own popup around them. Without it the preview content is ignored." },
];

<PropsTable rows={rootProps} />

export const rootAttrs = [
  { attribute: "data-chip", description: "The token element." },
  { attribute: "data-variant", values: '"primary" | "accent" | "warning"', description: "The active variant, for styling." },
];

<AttributesTable rows={rootAttrs} />

### Chip.Icon

Leading inline icon, baseline-aligned to the label and `aria-hidden`, so
decoration stays out of the chip's accessible name. Renders a `<span>` element.

### Chip.Label

The chip's text, which reads as part of the sentence around it. Renders a
`<span>` element.

### Chip.Preview

Marker child whose content becomes the preview body. Renders nothing inline:
`Chip.Root` inspects its children and routes this through `renderWithPreview`,
so the package never owns a popup.

export const previewProps = [
  { name: "children", type: "ReactNode", default: "(required)", description: "The hover-card body." },
];

<PropsTable rows={previewProps} />
