Tags Input

Allows entering multiple tags/values in a text input field.

React
Solid
import { TagsInput } from "~/components/tags-input";

export default function TagsInputBasicDemo() {
  return <TagsInput defaultValue={["React", "Solid"]} label="Frameworks" />;
}

Installation

CLI

Run the following command to add the component to your project:

npx @ark-preset/cli@latest add tags-input

Manual

Create the recipe file at src/components/recipes/tags-input.ts:

import { tv, type VariantProps } from "tailwind-variants";

export const tagsInputVariants = tv({
  slots: {
    root: "flex flex-col gap-1.5 w-full",
    label: "text-sm font-medium text-foreground select-none data-[disabled]:opacity-50",
    control: [
      "relative flex flex-wrap items-center gap-1 min-h-8 w-full rounded-md border border-input bg-background px-2.5 py-0.5 text-sm ring-offset-background",
      "has-[button]:pr-8",
      "focus-within:outline-none focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-1",
      "data-[disabled]:opacity-50",
      "data-[invalid]:border-destructive data-[invalid]:focus-within:ring-destructive",
    ],
    input: [
      "flex-1 min-w-16 h-auto text-base md:text-sm bg-transparent border-none outline-none",
      "placeholder:text-muted-foreground text-foreground",
    ],
    clearTrigger: [
      "absolute top-1/2 right-2 -translate-y-1/2",
      "flex items-center justify-center p-1 rounded",
      "bg-transparent border-none cursor-pointer",
      "text-muted-foreground hover:text-foreground hover:bg-muted",
      "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
    ],
    item: "inline-flex items-center outline-none cursor-default",
    itemPreview: [
      "inline-flex items-center gap-1 py-0.5 pl-2 pr-1 rounded-sm h-6",
      "bg-muted text-sm text-foreground outline-none",
      "data-[highlighted]:bg-primary/10 data-[highlighted]:text-primary",
    ],
    itemText: "font-medium text-sm",
    itemInput: [
      "min-w-16 px-2 py-0.5 text-base md:text-sm bg-muted border border-border rounded",
      "outline-none text-foreground",
    ],
    itemDeleteTrigger: [
      "flex items-center justify-center p-0.5 rounded",
      "bg-transparent border-none cursor-pointer text-muted-foreground",
      "hover:text-foreground",
      "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
    ],
  },
});

export type TagsInputVariants = VariantProps<typeof tagsInputVariants>;

Create the component directory and files.

src/components/tags-input/tags-input.base.tsx:

import { TagsInput as ArkTagsInput } from "@ark-ui/solid/tags-input";
import { createContext, useContext, splitProps, type Component } from "solid-js";
import { tagsInputVariants } from "../recipes/tags-input";

type TagsInputVariantContextValue = {
  disabled?: boolean;
};

const TagsInputVariantContext = createContext<TagsInputVariantContextValue>();

const useTagsInputVariant = () => useContext(TagsInputVariantContext);

const styles = tagsInputVariants();

const Root: Component<ArkTagsInput.RootProps> = (props) => {
  const [local, others] = splitProps(props, ["class", "disabled"]);
  return (
    <TagsInputVariantContext.Provider value={{ disabled: local.disabled }}>
      <ArkTagsInput.Root
        class={styles.root({ class: local.class })}
        disabled={local.disabled}
        {...others}
      />
    </TagsInputVariantContext.Provider>
  );
};

const RootProvider: Component<ArkTagsInput.RootProviderProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <TagsInputVariantContext.Provider value={{}}>
      <ArkTagsInput.RootProvider class={styles.root({ class: local.class })} {...others} />
    </TagsInputVariantContext.Provider>
  );
};

const Label: Component<ArkTagsInput.LabelProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkTagsInput.Label class={styles.label({ class: local.class })} {...others} />;
};

const Control: Component<ArkTagsInput.ControlProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkTagsInput.Control class={styles.control({ class: local.class })} {...others} />;
};

const Input: Component<ArkTagsInput.InputProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkTagsInput.Input class={styles.input({ class: local.class })} {...others} />;
};

const ClearTrigger: Component<ArkTagsInput.ClearTriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkTagsInput.ClearTrigger class={styles.clearTrigger({ class: local.class })} {...others} />
  );
};

const Item: Component<ArkTagsInput.ItemProps> = (props) => {
  const ctx = useTagsInputVariant();
  const [local, others] = splitProps(props, ["class", "disabled"]);
  return (
    <ArkTagsInput.Item
      class={styles.item({ class: local.class })}
      disabled={local.disabled ?? ctx?.disabled}
      {...others}
    />
  );
};

const ItemPreview: Component<ArkTagsInput.ItemPreviewProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkTagsInput.ItemPreview class={styles.itemPreview({ class: local.class })} {...others} />
  );
};

const ItemText: Component<ArkTagsInput.ItemTextProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkTagsInput.ItemText class={styles.itemText({ class: local.class })} {...others} />;
};

const ItemInput: Component<ArkTagsInput.ItemInputProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return <ArkTagsInput.ItemInput class={styles.itemInput({ class: local.class })} {...others} />;
};

const ItemDeleteTrigger: Component<ArkTagsInput.ItemDeleteTriggerProps> = (props) => {
  const [local, others] = splitProps(props, ["class"]);
  return (
    <ArkTagsInput.ItemDeleteTrigger
      class={styles.itemDeleteTrigger({ class: local.class })}
      {...others}
    />
  );
};

const Context = ArkTagsInput.Context;
const HiddenInput = ArkTagsInput.HiddenInput;

export const TagsInput = {
  Root,
  RootProvider,
  Label,
  Control,
  Input,
  ClearTrigger,
  Item,
  ItemPreview,
  ItemText,
  ItemInput,
  ItemDeleteTrigger,
  Context,
  HiddenInput,
};

export { TagsInputVariantContext, useTagsInputVariant };

src/components/tags-input/index.tsx:

import { Index, splitProps, type Component } from "solid-js";
import { TagsInput as TagsInputBase } from "./tags-input.base";
import { TagsInput as ArkTagsInput } from "@ark-ui/solid/tags-input";
import type { TagsInputVariants } from "../recipes/tags-input";

const TagsInputItem: Component<ArkTagsInput.ItemProps & TagsInputVariants> = (props) => {
  const [local, others] = splitProps(props, ["children"]);
  return (
    <TagsInputBase.Item {...others}>
      <TagsInputBase.ItemPreview>
        <TagsInputBase.ItemText>{local.children}</TagsInputBase.ItemText>
        <TagsInputBase.ItemDeleteTrigger>
          <svg
            xmlns="http://www.w3.org/2000/svg"
            width="14"
            height="14"
            viewBox="0 0 24 24"
            fill="none"
            stroke="currentColor"
            stroke-width="2"
            stroke-linecap="round"
            stroke-linejoin="round"
          >
            <path d="M18 6 6 18" />
            <path d="m6 6 12 12" />
          </svg>
        </TagsInputBase.ItemDeleteTrigger>
      </TagsInputBase.ItemPreview>
      <TagsInputBase.ItemInput />
    </TagsInputBase.Item>
  );
};

type TagsInputProps = ArkTagsInput.RootProps & {
  label?: string;
};

const TagsInput: Component<TagsInputProps> = (props) => {
  const [local, others] = splitProps(props, ["class", "children", "disabled", "label"]);
  return (
    <TagsInputBase.Root class={local.class} disabled={local.disabled} {...others}>
      <TagsInputBase.Context>
        {(api) => (
          <>
            {local.label && <TagsInputBase.Label>{local.label}</TagsInputBase.Label>}
            {local.children}
            <TagsInputBase.Control>
              <Index each={api().value}>
                {(value, index) => (
                  <TagsInputBase.Item index={index} value={value()}>
                    <TagsInputBase.ItemPreview>
                      <TagsInputBase.ItemText>{value()}</TagsInputBase.ItemText>
                      <TagsInputBase.ItemDeleteTrigger>
                        <svg
                          xmlns="http://www.w3.org/2000/svg"
                          width="14"
                          height="14"
                          viewBox="0 0 24 24"
                          fill="none"
                          stroke="currentColor"
                          stroke-width="2"
                          stroke-linecap="round"
                          stroke-linejoin="round"
                        >
                          <path d="M18 6 6 18" />
                          <path d="m6 6 12 12" />
                        </svg>
                      </TagsInputBase.ItemDeleteTrigger>
                    </TagsInputBase.ItemPreview>
                    <TagsInputBase.ItemInput />
                  </TagsInputBase.Item>
                )}
              </Index>
              <TagsInputBase.Input />
            </TagsInputBase.Control>
          </>
        )}
      </TagsInputBase.Context>
      <TagsInputBase.HiddenInput />
    </TagsInputBase.Root>
  );
};

export { TagsInput, TagsInputItem, TagsInputBase };

export { tagsInputVariants, type TagsInputVariants } from "../recipes/tags-input";

Note: Make sure your project has the Tailwind CSS theme variables set up (--background, --foreground, --ring, --border, etc.) or override the utility classes to match your design system.

Usage

Basic Usage

React
Solid
import { TagsInput } from "~/components/tags-input";

export default function TagsInputBasicDemo() {
  return <TagsInput defaultValue={["React", "Solid"]} label="Frameworks" />;
}

Controlled Value

Use value and onValueChange to control the tag values externally:

Tags: Solid

Solid
import { createSignal } from "solid-js";
import { TagsInput } from "~/components/tags-input";

export default function TagsInputControlledDemo() {
  const [value, setValue] = createSignal(["Solid"]);
  return (
    <div class="space-y-6">
      <div>
        <p class="text-sm text-muted-foreground mb-2">Tags: {value().join(", ")}</p>
        <TagsInput
          value={value()}
          onValueChange={(e: { value: string[] }) => setValue(e.value)}
          label="Frameworks"
        />
      </div>
    </div>
  );
}

Disabled

Use the disabled prop to disable the tags input:

Disabled tags input

React
Solid
import { TagsInput } from "~/components/tags-input";

export default function TagsInputDisabledDemo() {
  return (
    <div class="space-y-6">
      <div>
        <p class="text-sm text-muted-foreground mb-2">Disabled tags input</p>
        <TagsInput defaultValue={["React", "Solid"]} label="Frameworks" disabled />
      </div>
    </div>
  );
}

Invalid State

Use the invalid prop (from Ark UI) to mark the input as invalid:

Invalid tags input

React
Solid
import { TagsInput } from "~/components/tags-input";

export default function TagsInputInvalidDemo() {
  return (
    <div class="space-y-6">
      <div>
        <p class="text-sm text-muted-foreground mb-2">Invalid tags input</p>
        <TagsInput defaultValue={["React", "Solid"]} label="Frameworks" invalid />
      </div>
    </div>
  );
}

Advanced Usage

When the composite TagsInput doesn’t provide enough control, import the raw primitive parts from the base file directly:

import { TagsInput } from "~/components/tags-input/tags-input.base";

Or import TagsInputBase (the raw parts namespace) from the composite entry point:

import { TagsInputBase } from "~/components/tags-input";

RootProvider Pattern

For full control over the tags input machine, use useTagsInput with TagsInputBase.RootProvider:

Tags: React, Solid

React
Solid
import { Index } from "solid-js";
import { TagsInputBase } from "~/components/tags-input";
import { useTagsInput } from "@ark-ui/solid/tags-input";

export default function TagsInputRootProviderDemo() {
  const tagsInput = useTagsInput({ defaultValue: ["React", "Solid"] });
  return (
    <div class="space-y-6">
      <div>
        <p class="text-sm text-muted-foreground mb-2">Tags: {tagsInput().value.join(", ")}</p>
        <TagsInputBase.RootProvider value={tagsInput}>
          <TagsInputBase.Label>Frameworks</TagsInputBase.Label>
          <TagsInputBase.Control>
            <TagsInputBase.Context>
              {(api: any) => (
                <Index each={api().value}>
                  {(value, index) => (
                    <TagsInputBase.Item index={index} value={value()}>
                      <TagsInputBase.ItemPreview>
                        <TagsInputBase.ItemText>{value()}</TagsInputBase.ItemText>
                        <TagsInputBase.ItemDeleteTrigger>
                          <svg
                            xmlns="http://www.w3.org/2000/svg"
                            width="14"
                            height="14"
                            viewBox="0 0 24 24"
                            fill="none"
                            stroke="currentColor"
                            stroke-width="2"
                            stroke-linecap="round"
                            stroke-linejoin="round"
                          >
                            <path d="M18 6 6 18" />
                            <path d="m6 6 12 12" />
                          </svg>
                        </TagsInputBase.ItemDeleteTrigger>
                      </TagsInputBase.ItemPreview>
                      <TagsInputBase.ItemInput />
                    </TagsInputBase.Item>
                  )}
                </Index>
              )}
            </TagsInputBase.Context>
            <TagsInputBase.Input placeholder="Add a tag..." />
            <TagsInputBase.ClearTrigger>
              <svg
                xmlns="http://www.w3.org/2000/svg"
                width="16"
                height="16"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
              >
                <path d="M18 6 6 18" />
                <path d="m6 6 12 12" />
              </svg>
            </TagsInputBase.ClearTrigger>
          </TagsInputBase.Control>
        </TagsInputBase.RootProvider>
      </div>
    </div>
  );
}

Custom Tag Rendering with TagsInputItem

For custom tag rendering (e.g., custom delete icon), use TagsInputItem:

import { Index } from "solid-js";
import { TagsInput, TagsInputItem } from "~/components/tags-input";

export function CustomItemDemo() {
  return (
    <TagsInput defaultValue={["React", "Solid"]}>
      <TagsInputBase.Label>Frameworks</TagsInputBase.Label>
      <TagsInputBase.Control>
        <Index each={api().value}>
          {(value, index) => (
            <TagsInputItem index={index} value={value()}>
              {value()}
            </TagsInputItem>
          )}
        </Index>
        <TagsInputBase.Input />
      </TagsInputBase.Control>
    </TagsInput>
  );
}

API Reference

See the Ark UI TagsInput documentation.