Skip to main content

Taginput

Overview​

The Taginput component provides a tag/chip input field for managing multiple tags.

It supports autocomplete suggestions, custom tag styling, and various input behaviors.


Import​

import { Taginput } from '@allxsmith/bestax-bulma';

Usage​

The component is exported as Taginput; a deprecated TagInput alias names the same component for imports written with camel-case spelling, and TagInputProps is the matching deprecated alias of the TaginputProps type.

Basic Taginput​

Simple tag input without suggestions.

import type { TaginputTag } from '@allxsmith/bestax-bulma';

function example() {
  const [tags, setTags] = useState<TaginputTag[]>(['React', 'TypeScript']);

  return (
    <Block>
      <Taginput value={tags} onChange={setTags} placeholder="Add a tag..." />
      <Paragraph mt="2" textColor="grey">
        Tags: {tags.join(', ')}
      </Paragraph>
    </Block>
  );
}


With Autocomplete​

Tag input with suggestion dropdown.

import type { TaginputTag } from '@allxsmith/bestax-bulma';

function example() {
  const [tags, setTags] = useState<TaginputTag[]>(['React']);
  const suggestions = [
    'React',
    'Vue',
    'Angular',
    'Svelte',
    'TypeScript',
    'JavaScript',
    'Python',
    'Go',
  ];

  return (
    <Taginput
      value={tags}
      onChange={setTags}
      data={suggestions}
      placeholder="Add frameworks..."
      openOnFocus
    />
  );
}


Restrict to Suggestions​

Only allow tags from the suggestion list.

import type { TaginputTag } from '@allxsmith/bestax-bulma';

function example() {
  const [tags, setTags] = useState<TaginputTag[]>([]);
  const categories = [
    'Bug',
    'Feature',
    'Documentation',
    'Enhancement',
    'Question',
  ];

  return (
    <Block>
      <Taginput
        value={tags}
        onChange={setTags}
        data={categories}
        allowNew={false}
        placeholder="Select categories..."
        openOnFocus
      />
      <Paragraph mt="2" textColor="grey" textSize="7">
        Only predefined categories can be selected
      </Paragraph>
    </Block>
  );
}


Tag Colors​

Tags with different color variants.

<Block display="flex" flexDirection="column">
  <Taginput
    defaultValue={['Primary']}
    tagColor="primary"
    placeholder="Primary tags..."
  />
  <Taginput
    defaultValue={['Success']}
    tagColor="success"
    placeholder="Success tags..."
  />
  <Taginput
    defaultValue={['Info']}
    tagColor="info"
    placeholder="Info tags..."
  />
  <Taginput
    defaultValue={['Warning']}
    tagColor="warning"
    placeholder="Warning tags..."
  />
  <Taginput
    defaultValue={['Danger']}
    tagColor="danger"
    placeholder="Danger tags..."
  />
  <Taginput
    defaultValue={['Dark']}
    tagColor="dark"
    placeholder="Dark tags..."
  />
</Block>


Size Variants​

Tag inputs in different sizes.

<Block display="flex" flexDirection="column">
  <Taginput
    defaultValue={['Small']}
    size="small"
    tagColor="primary"
    placeholder="Small..."
  />
  <Taginput
    defaultValue={['Normal']}
    tagColor="primary"
    placeholder="Normal..."
  />
  <Taginput
    defaultValue={['Medium']}
    size="medium"
    tagColor="primary"
    placeholder="Medium..."
  />
  <Taginput
    defaultValue={['Large']}
    size="large"
    tagColor="primary"
    placeholder="Large..."
  />
</Block>


Maximum Tags​

Limit the number of tags allowed.

import type { TaginputTag } from '@allxsmith/bestax-bulma';

function example() {
  const [tags, setTags] = useState<TaginputTag[]>(['One', 'Two']);

  return (
    <Block>
      <Taginput
        value={tags}
        onChange={setTags}
        maxTags={3}
        tagColor="info"
        placeholder="Max 3 tags..."
      />
      <Paragraph mt="2" textColor="grey" textSize="7">
        {3 - tags.length} tags remaining
      </Paragraph>
    </Block>
  );
}


Allow Duplicates​

Enable duplicate tag values.

import type { TaginputTag } from '@allxsmith/bestax-bulma';

function example() {
  const [tags, setTags] = useState<TaginputTag[]>(['Tag']);

  return (
    <Taginput
      value={tags}
      onChange={setTags}
      allowDuplicates
      tagColor="warning"
      placeholder="Duplicates allowed..."
    />
  );
}


Custom Confirm Keys​

Change which keys create a new tag.

import type { TaginputTag } from '@allxsmith/bestax-bulma';

function example() {
  const [tags, setTags] = useState<TaginputTag[]>([]);

  return (
    <Block>
      <Taginput
        value={tags}
        onChange={setTags}
        confirmKeys={['Enter', 'Tab', ' ']}
        tagColor="success"
        placeholder="Press Enter, Tab, or Space..."
      />
      <Paragraph mt="2" textColor="grey" textSize="7">
        Space also creates a new tag
      </Paragraph>
    </Block>
  );
}


Read-only and Disabled​

Display modes for tags.

<Block display="flex" flexDirection="column">
  <Block>
    <Paragraph mb="1">Read-only:</Paragraph>
    <Taginput defaultValue={['React', 'TypeScript']} readonly tagColor="info" />
  </Block>
  <Block>
    <Paragraph mb="1">Disabled:</Paragraph>
    <Taginput defaultValue={['React', 'TypeScript']} disabled tagColor="info" />
  </Block>
</Block>


Non-closable Tags​

Tags without the delete button.

<Taginput
  defaultValue={['Fixed', 'Tags']}
  closable={false}
  tagColor="primary"
  placeholder="Cannot remove tags..."
/>


Context-Aware Rendering​

The Taginput component is context-aware: it detects whether it is already inside a Field and adjusts its rendering accordingly. This means you can use it standalone with a label prop (it wraps itself in a Field), or inside a Field (it skips rendering its own).

note

Taginput renders no Control of its own. The Control wrapper in the "With Field and Control Wrappers" example below is shown for layout consistency but does not change the component's internal rendering.

Default (with label)​

The simplest usage — the component automatically renders its own Field wrapper.

<Taginput
  label="Tags"
  defaultValue={['React', 'TypeScript']}
  placeholder="Add a tag..."
  tagColor="primary"
/>


With Field Wrapper​

When you need manual control over the Field layout (e.g., horizontal forms), wrap the component in Field. The component detects it's inside a Field and skips rendering its own.

A labeled Field that holds the Taginput directly names its input with no extra wiring. In a horizontal form the label sits on the outer Field and the Taginput in an inner one, which starts its own scope, so these examples wire the label by hand with labelProps={{ htmlFor }} and a matching id.

function example() {
  return (
    <Field horizontal label="Tags" labelProps={{ htmlFor: 'tags-field' }}>
      <Field.Body>
        <Field>
          <Taginput
            id="tags-field"
            defaultValue={['React', 'TypeScript']}
            placeholder="Add a tag..."
            tagColor="primary"
          />
        </Field>
      </Field.Body>
    </Field>
  );
}


With Field and Control Wrappers​

For full manual composition, wrap in both Field and Control. The Field wrapper is detected and its own Field is skipped.

For an icon, use Taginput's own icon prop rather than <Control iconLeftName> — Bulma's has-icons-left only adjusts padding on .input/.select, not on .taginput, so wrapping with Control's icon causes the icon to overlap the tags.

function example() {
  return (
    <Field
      horizontal
      label="Tags"
      labelProps={{ htmlFor: 'tags-field-control' }}
    >
      <Field.Body>
        <Field>
          <Control>
            <Taginput
              id="tags-field-control"
              icon="tags"
              defaultValue={['React', 'TypeScript']}
              placeholder="Add a tag..."
              tagColor="primary"
            />
          </Control>
        </Field>
      </Field.Body>
    </Field>
  );
}

Inside a Control with no Field around it, Taginput renders no Field of its own either, unless you give it label, message, horizontal or fieldClassName. Those need a Field, so with any of them it keeps its own Field inside the Control and warns in development. Wrap the Control in a Field, as above, and set the label, horizontal and class name on that Field instead.


Keyboard Navigation​

KeyAction
EnterCreate tag from input
,Create tag from input (default)
BackspaceRemove last tag (when input empty)
↓Open/navigate suggestions
↑Navigate suggestions up
EscapeClose suggestions dropdown

Controlled vs Uncontrolled​

Controlled Mode​

Use value and onChange to manage state externally:

import type { TaginputTag } from '@allxsmith/bestax-bulma';

const [tags, setTags] = useState<TaginputTag[]>(['React']);
<Taginput value={tags} onChange={setTags} />;

Uncontrolled Mode​

Use defaultValue for internal state management:

<Taginput defaultValue={['React', 'TypeScript']} />

Form Submission​

Taginput is an HTML form element. Pass a name prop and one hidden <input> per tag is rendered, producing standard form-encoded array submission (e.g., tags=react&tags=vue&tags=angular). Standard server-side parsers (PHP, Express body-parser, etc.) handle this format natively.

PropDescription
nameForm field name. When set, one hidden input per tag is rendered.
formOptional id of the form the hidden inputs belong to.
function TaginputFormDemo() {
  const [submitted, setSubmitted] = React.useState('');
  return (
    <form
      onSubmit={e => {
        e.preventDefault();
        const fd = new FormData(e.currentTarget);
        setSubmitted(JSON.stringify(Array.from(fd.entries()), null, 2));
      }}
    >
      <Taginput
        name="tags"
        defaultValue={['react', 'vue', 'angular']}
        placeholder="Add a tag…"
      />
      <div style={{ marginTop: '1rem' }}>
        <button type="submit" className="button is-primary">
          Submit
        </button>
      </div>
      {submitted && <pre style={{ marginTop: '1rem' }}>{submitted}</pre>}
    </form>
  );
}


Accessibility​

  • The label prop is automatically associated with the inner text input (htmlFor plus a generated id, or your own id if you pass one). While the label is wired it also names the input; without one, the input falls back to aria-label="Add tag". Inside a labeled Field, that Field's label names the input the same way and replaces the fallback.
  • A user-supplied id is applied to the inner input (the labellable control), not the wrapper div.
  • When maxTags is reached the text input is not rendered, so the label renders without a for attribute.
  • Tags are keyboard navigable
  • Delete buttons have proper aria-label
  • With data, the input is a role="combobox" with aria-autocomplete="list" and aria-haspopup="listbox". Its aria-expanded is true only while the suggestion list is on screen, and while it is, aria-controls points at the list. While no suggestion matches there's no list, so it stays false. Without data there's never a list, so the input stays a plain text field
  • Focus stays in the input, which points aria-activedescendant at the highlighted suggestion as the arrow keys or the mouse move it
  • The list is a role="listbox" named through aria-labelledby by the label that names the input, when that label has an id: the label prop's label always has one, and so does a Field's label that the Field wires itself. A Field label you wire by hand with labelProps.htmlFor, which is how the horizontal examples above name the Taginput, needs an id in its labelProps as well. Otherwise the list falls back to aria-label="Suggestions"
  • The list holds only the suggestions, each a role="option" with an id of its own, and the highlighted one has aria-selected="true"

  • Autocomplete - For single value autocomplete
  • Input - For basic text input
  • Tag - For displaying tags

Additional Resources​

Pro Tip

Set allowNew={false} when you need strict control over allowed values, such as selecting from predefined categories or labels.


Props​

PropTypeDefaultDescription
labelReact.ReactNode—Field label. Automatically associated with the text input via htmlFor — uses your id when provided, otherwise a generated one. Not wired when the tag limit is reached (the input is not rendered). Dropped inside an outer Field, whose own label associates instead when that Field generates a target id (not grouped/hasAddons, no explicit labelProps.htmlFor) and you set no id here; it then replaces the aria-label="Add tag" fallback too. With data, the label naming the input also names the open suggestion list, through aria-labelledby, when it has an id: this label always does, and an outer Field's does unless you wired it by hand without an id in its labelProps. Otherwise the list is named "Suggestions".
labelPropsReact.LabelHTMLAttributes<HTMLLabelElement> & { [key: string]: unknown; }—Props for the label element. An explicit htmlFor here overrides the automatic association (no id is generated then). With data, while the label names the input, the suggestion list's aria-labelledby points at it, through an id here when you give one and a generated one otherwise.
idstring—Applied to the inner text input (the labellable control), not the wrapper div.
valueTaginputTag[]—The current tags (controlled).
defaultValueTaginputTag[][]Default tags (uncontrolled).
datastring[]—Autocomplete suggestions. With data set, even to an empty array, the text input is a role="combobox" that controls the suggestion list and points aria-activedescendant at the highlighted suggestion. Without it there's no list, so the input stays a plain text field.
placeholderstring—Placeholder text when no tags.
fieldstring'label'Object property to use as display field.
allowNewbooleantrueAllow creating new tags not in suggestions.
allowDuplicatesbooleanfalseAllow duplicate tags.
openOnFocusbooleanfalseOpen autocomplete dropdown on focus.
removeOnKeysbooleantrueRemove tag on backspace. Default: true.
confirmKeysstring[]['Enter', ',']Keys to confirm tag creation. Default: ['Enter', ','].
closablebooleantrueShow close button on tags. Default: true.
attachedbooleanfalseAttach tags visually.
maxTagsnumber—Maximum number of tags allowed.
maxlengthnumber—Maximum length of input.
disabledbooleanfalseWhether the input is disabled.
readonlybooleanfalseWhether the input is read-only.
roundedbooleanfalseApplies rounded corners to the tags.
ellipsisbooleanfalseTruncates long tag text with ellipsis.
hasCounterbooleantrueShows a counter of the number of tags.
onPasteSeparatorsstring[][',']Characters that split pasted text into tags.
beforeAdding(tag: string) => boolean—Validation function called before adding a tag.
createTag(input: string) => TaginputTag—Custom function for creating tag objects from input.
keepFirstbooleanfalseKeeps the first autocomplete suggestion highlighted.
keepOpenbooleantrueKeeps the autocomplete dropdown open after selection.
loadingbooleanfalseShows a loading spinner in the input. Under prefers-reduced-motion: reduce the spinner stops and stays drawn (with bestax's CSS loaded).
ariaCloseLabelstring—ARIA label for tag close buttons.
iconstring—Icon name for the input field.
iconLibrary'fa' | 'mdi' | 'ion' | 'material-icons' | 'material-symbols'—Icon library to use.
iconVariantstring—Icon style variant (e.g., 'solid', 'outlined').
iconFeaturesstring | string[]—Additional icon modifiers.
color'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'—Input color variant.
tagColor'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger' | 'dark' | 'light'—Tag color variant.
size'small' | 'medium' | 'large'—Size variant.
namestring—Form field name. When set, one hidden input per tag is rendered.
formstring—Optional id of the form the hidden inputs belong to.
onChange(tags: TaginputTag[]) => void—Callback when tags change.
onAdd(tag: TaginputTag) => void—Callback when tag is added.
onRemove(tag: TaginputTag, index: number) => void—Callback when tag is removed.
onTyping(value: string) => void—Callback when typing in input.
tagTemplate(tag: TaginputTag) => React.ReactNode—Custom render for tags.
labelSize'small' | 'normal' | 'medium' | 'large'—Size for the label (used in horizontal layouts).
horizontalbooleanfalseHorizontal field layout.
messageReact.ReactNode—Help/validation message below the input.
messageColor'primary' | 'link' | 'info' | 'success' | 'warning' | 'danger'—Bulma color for the message.
fieldClassNamestring—Additional CSS classes for the Field wrapper.
classNamestring—Additional CSS classes.
refReact.Ref<HTMLInputElement>—Ref forwarded to the input element.
...All standard <div> attributes and Bulma helper props—See Helper Props

TaginputTag​

Can be either a string or an object with:

PropTypeDescription
valuestringThe tag value.
labelstringDisplay label (optional).

CSS & Sass Variables​

Taginput registers these variables on its own .taginput element. Override them there (or via className) — a value set on an ancestor is only inherited, and loses to the component-level declaration. See Theme.

CSS VariableSass VariableDefault
--bulma-taginput-height$taginput-heightauto
--bulma-taginput-min-height$taginput-min-height2.5em
--bulma-taginput-padding$taginput-padding0.375rem 0.5rem
--bulma-taginput-border-color$taginput-border-colorvar(--bulma-border)
--bulma-taginput-border-radius$taginput-border-radiusvar(--bulma-radius)
--bulma-taginput-background$taginput-backgroundvar(--bulma-scheme-main)
--bulma-taginput-tag-gap$taginput-tag-gap0.25rem
--bulma-taginput-dropdown-max-height$taginput-dropdown-max-height200px
--bulma-taginput-transition-duration$taginput-transition-durationvar(--bulma-duration)