Tag

An interactive chip. If it cannot be clicked or removed, it is a Badge.

Display Tag.jsx Tag.d.ts Tag.md

Playground

Change a prop and watch it. The code below follows along.

Install and import

Tag is in @dovetail-ds/react. Install it with React, if you haven't:

npm install @dovetail-ds/react react react-dom
import { Tag } from "@dovetail-ds/react";

Import @dovetail-ds/react/styles.css once at your app's root, then your theme after it. Setup covers the stylesheets, fonts and dark mode.

Guidelines

An interactive chip. If it cannot be clicked or removed, it is a Badge.

Use it when

  • Filter chips the user toggles.
  • Values the user entered and can remove: recipients, labels, applied filters.

Don't use it when

  • It is read-only status. Use Badge.
  • It is a set of mutually exclusive options in a form. Use Radio.

Example

<Inline gap="xs">
  <Tag selected onClick={() => toggle("open")}>Open</Tag>
  <Tag onClick={() => toggle("closed")}>Closed</Tag>
  <Tag onRemove={() => clear("priority")}>Priority: high</Tag>
</Inline>

Accessibility

With an onClick, Tag becomes role="button" with aria-pressed and a tab stop. The remove button gets its own label derived from the tag text.

Content

Sentence case. Applied filters read as "Field: value".

Props

The typed contract, from Tag.d.ts.

import * as React from "react";

/** Interactive chip: a filter that can be toggled, or an entered value that can be removed. */
export interface TagProps extends React.HTMLAttributes<HTMLSpanElement> {
  /** Toggled-on state. Sets aria-pressed when the tag is clickable. */
  selected?: boolean;
  /** Renders a remove button. */
  onRemove?: () => void;
  disabled?: boolean;
  children?: React.ReactNode;
}

export declare function Tag(props: TagProps): React.JSX.Element;

Tokens it reads

Every custom property Tag.jsx resolves, read from the source: 15 semantic, 1 primitive. Component tier first, because that is the one to reach for.

  • semantic Shared roles. Override one and everything using that role moves with it.
  • primitive Raw values. A component reading one directly is a deliberate exception.
TokenTierDeclared as
--dt-border-defaultsemanticvar(--dt-color-neutral-200)
--dt-border-selectedsemanticvar(--dt-color-neutral-900)
--dt-border-width-defaultsemanticvar(--dt-dim-hair)
--dt-motion-microsemanticvar(--dt-duration-100) var(--dt-easing-standard)
--dt-radius-pillsemanticvar(--dt-radius-raw-full)
--dt-size-control-smsemanticvar(--dt-dim-8)
--dt-space-inline-2xssemanticvar(--dt-dim-1)
--dt-space-inset-smsemanticvar(--dt-dim-3)
--dt-surface-basesemanticvar(--dt-color-white)
--dt-surface-selectedsemanticvar(--dt-color-neutral-100)
--dt-surface-subtlesemanticvar(--dt-color-neutral-050)
--dt-text-label-md-familysemanticvar(--dt-font-family-secondary)
--dt-text-label-md-sizesemanticvar(--dt-font-size-sm)
--dt-text-on-selectedsemanticvar(--dt-color-neutral-900)
--dt-text-primarysemanticvar(--dt-color-neutral-900)
--dt-font-weight-mediumprimitive500

Source

React only, no dependencies, every value a token.

import React from "react";

export function Tag({ selected = false, onRemove, disabled = false, children, style, ...rest }) {
  const [hover, setHover] = React.useState(false);
  const interactive = !!rest.onClick;
  return (
    <span
      onMouseEnter={() => setHover(true)}
      onMouseLeave={() => setHover(false)}
      role={interactive ? "button" : undefined}
      tabIndex={interactive && !disabled ? 0 : undefined}
      aria-pressed={interactive ? selected : undefined}
      style={{
        display: "inline-flex", alignItems: "center", gap: "var(--dt-space-inline-2xs)",
        fontFamily: "var(--dt-text-label-md-family)", fontSize: "var(--dt-text-label-md-size)",
        lineHeight: 1, fontWeight: "var(--dt-font-weight-medium)",
        height: "var(--dt-size-control-sm)", padding: "0 var(--dt-space-inset-sm)",
        borderRadius: "var(--dt-radius-pill)",
        border: `var(--dt-border-width-default) solid ${selected ? "var(--dt-border-selected)" : "var(--dt-border-default)"}`,
        background: selected ? "var(--dt-surface-selected)" : hover && interactive ? "var(--dt-surface-subtle)" : "var(--dt-surface-base)",
        color: selected ? "var(--dt-text-on-selected)" : "var(--dt-text-primary)",
        cursor: disabled ? "not-allowed" : interactive ? "pointer" : "default",
        opacity: disabled ? 0.6 : 1,
        transition: "background var(--dt-motion-micro), border-color var(--dt-motion-micro)",
        ...style,
      }}
      {...rest}
    >
      {children}
      {onRemove && (
        <button
          type="button"
          aria-label={`Remove ${typeof children === "string" ? children : "tag"}`}
          onClick={(e) => { e.stopPropagation(); onRemove(); }}
          style={{ display: "flex", padding: 0, border: 0, background: "none", color: "inherit", cursor: "pointer" }}
        >
          <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round"><path d="M18 6 6 18" /><path d="m6 6 12 12" /></svg>
        </button>
      )}
    </span>
  );
}

In context

Tag also appears on the display reference cards, alongside the rest of its family.