Tags Input
Allows entering multiple tags/values in a text input field.
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-inputManual
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
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
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
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
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
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.