Tag Input

  • CSS
  • Sass

Press Enter or comma to add a skill, Backspace to remove the last one.

@use 'sprucecss/scss/spruce' as *;

.tag-input {
  @include layout-stack('xs');

  &__box {
    align-items: center;
    background-color: color('background', 'form');
    border: config('border-width', $form-control, false) solid color('border', 'form');
    border-radius: config('border-radius', $form-control, false);
    cursor: text;
    display: flex;
    flex-wrap: wrap;
    gap: spacer('xxs');
    padding: spacer('xxs');
    @include transition($property: 'border-color, box-shadow');

    &:focus-within {
      @include short-ring('input');
    }
  }

  &__list {
    @include clear-list;
    display: contents;
  }

  &__tag {
    align-items: center;
    background-color: color-alpha('primary', 0.12);
    border-radius: radius('sm');
    color: color('heading');
    display: inline-flex;
    font-size: config('font-size-sm', $typography);
    gap: spacer('xxs');
    margin: 0 !important;
    padding: 0.2em 0.25em 0.2em 0.6em;
  }

  &__remove {
    @include clear-btn;
    border-radius: radius('sm');
    font-size: 1.15em;
    line-height: 1;
    padding: 0 0.3em;

    &:hover,
    &:focus-visible {
      background-color: color-alpha('primary', 0.2);
      color: color('primary');
    }

    &:focus-visible {
      outline: 2px solid color('focus-ring');
    }
  }

  &__field {
    background: transparent;
    border: 0;
    color: color('text', 'form');
    flex: 1 0 8rem;
    font: inherit;
    padding: 0.25em 0.5em;

    &:focus {
      outline: none;
    }
  }
}
<div class="tag-input" data-tag-input>
    <label class="form-label" for="tag-input-field">Skills</label>
    <div class="tag-input__box">
        <ul class="tag-input__list" role="list" aria-label="Selected skills">
            <li class="tag-input__tag"><span>CSS</span><button class="tag-input__remove" type="button" aria-label="Remove CSS">×</button></li>
            <li class="tag-input__tag"><span>Sass</span><button class="tag-input__remove" type="button" aria-label="Remove Sass">×</button></li>
        </ul>
        <input class="tag-input__field" id="tag-input-field" type="text" aria-describedby="tag-input-help" autocomplete="off">
    </div>
    <p class="form-description" id="tag-input-help">Press Enter or comma to add a skill, Backspace to remove the last one.</p>
    <p class="sr-only" aria-live="polite" data-tag-status></p>
    <input type="hidden" name="skills" value="CSS,Sass" data-tag-value>
</div>
(() => {
  document.querySelectorAll('[data-tag-input]').forEach((component) => {
    const list = component.querySelector('.tag-input__list');
    const field = component.querySelector('.tag-input__field');
    const status = component.querySelector('[data-tag-status]');
    const hidden = component.querySelector('[data-tag-value]');

    const values = () => [...list.querySelectorAll('.tag-input__tag span')].map((tag) => tag.textContent);
    const sync = () => { hidden.value = values().join(','); };

    const remove = (tag) => {
      const name = tag.querySelector('span').textContent;

      tag.remove();
      sync();
      status.textContent = `${name} removed.`;
      field.focus();
    };

    const add = (value) => {
      const name = value.trim();

      if (!name || values().some((item) => item.toLowerCase() === name.toLowerCase())) {
        return;
      }

      const tag = document.createElement('li');
      const label = document.createElement('span');
      const button = document.createElement('button');

      tag.className = 'tag-input__tag';
      label.textContent = name;
      button.className = 'tag-input__remove';
      button.type = 'button';
      button.textContent = '×';
      button.setAttribute('aria-label', `Remove ${name}`);
      tag.append(label, button);
      list.append(tag);
      sync();
      status.textContent = `${name} added.`;
    };

    component.querySelector('.tag-input__box').addEventListener('click', (event) => {
      const removeButton = event.target.closest('.tag-input__remove');

      if (removeButton) {
        remove(removeButton.closest('.tag-input__tag'));
      } else {
        field.focus();
      }
    });

    field.addEventListener('keydown', (event) => {
      if (event.key === 'Enter' || event.key === ',') {
        event.preventDefault();
        add(field.value);
        field.value = '';
      } else if (event.key === 'Backspace' && !field.value) {
        const last = list.querySelector('.tag-input__tag:last-child');

        if (last) {
          remove(last);
        }
      }
    });
  });
})();

Tag Input

On this page

A text input that turns entries into removable tags (chips), for skills, labels or email recipients.

Technical Details

  • Enter or , adds a tag, Backspace in an empty field removes the last one, and duplicates are ignored.
  • Every tag has a remove button with a specific label ("Remove CSS"). After a removal the focus returns to the input.
  • Additions and removals are announced through a visually hidden live region.
  • The values are mirrored to a hidden input (comma separated), so the component works in a regular form submit.
  • The box borrows the form control tokens and the short-ring('input') focus ring, so it looks like any other Spruce input.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.