GitHub

Input Tag

A tag / token multiselect. Type a value and press Enter to add a removable chip, or pick from an optional suggestion list. Form-associated via ElementInternals — the selected tokens submit with the enclosing form.

Stable wc import '@esa/ecology/esa-input-tag'; Angular usage

Preview

<esa-input-tag label="Tags" placeholder="Add a tag"></esa-input-tag>
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@esa/ecology/esa-input-tag';

@Component({
  selector: 'app-input-tag-demo',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <esa-input-tag label="Tags" placeholder="Add a tag"></esa-input-tag>
  `,
})
export class InputTagDemo {}

Suggestions

Pass an options array (as a property) to drive a filtered suggestion dropdown. Typed values and picked suggestions coexist — anything not in the list is added as a free-form token.

<esa-input-tag label="Species" placeholder="Search or add a species"></esa-input-tag>
<!-- el.options = [{ value, label }, ...] -->
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@esa/ecology/esa-input-tag';

@Component({
  selector: 'app-input-tag-demo',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <esa-input-tag label="Species" placeholder="Search or add a species"></esa-input-tag>
    <!-- el.options = [{ value, label }, ...] -->
  `,
})
export class InputTagDemo {}

Sizes

<esa-input-tag label="Extra small" size="xs"></esa-input-tag>
<esa-input-tag label="Small" size="sm"></esa-input-tag>
<esa-input-tag label="Medium" size="md"></esa-input-tag>
<esa-input-tag label="Large" size="lg"></esa-input-tag>
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@esa/ecology/esa-input-tag';

@Component({
  selector: 'app-input-tag-demo',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <esa-input-tag label="Extra small" size="xs"></esa-input-tag>
    <esa-input-tag label="Small" size="sm"></esa-input-tag>
    <esa-input-tag label="Medium" size="md"></esa-input-tag>
    <esa-input-tag label="Large" size="lg"></esa-input-tag>
  `,
})
export class InputTagDemo {}

States

The required asterisk, helper text, and disabled state are attribute-driven. Disabled fields keep their chips but drop the remove buttons. error-text replaces help-text in the slot below the field and reddens the border, so the two never stack.

<esa-input-tag label="Required" required placeholder="At least one tag"></esa-input-tag>
<esa-input-tag label="With help" help-text="Press Enter to add each tag."></esa-input-tag>
<esa-input-tag label="With error" error-text="Add at least one tag."></esa-input-tag>
<esa-input-tag label="Disabled" disabled></esa-input-tag>
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@esa/ecology/esa-input-tag';

@Component({
  selector: 'app-input-tag-demo',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <esa-input-tag label="Required" required placeholder="At least one tag"></esa-input-tag>
    <esa-input-tag label="With help" help-text="Press Enter to add each tag."></esa-input-tag>
    <esa-input-tag label="With error" error-text="Add at least one tag."></esa-input-tag>
    <esa-input-tag label="Disabled" disabled></esa-input-tag>
  `,
})
export class InputTagDemo {}

Form participation

The element is form-associated. Submitting logs the FormData entry for tags — a comma-joined list of token values.

<form>
  <esa-input-tag label="Tags" name="tags" placeholder="Add then submit"></esa-input-tag>
  <button type="submit">Submit</button>
</form>
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import '@esa/ecology/esa-input-tag';

@Component({
  selector: 'app-input-tag-demo',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <form>
      <esa-input-tag label="Tags" name="tags" placeholder="Add then submit"></esa-input-tag>
      <button type="submit">Submit</button>
    </form>
  `,
})
export class InputTagDemo {}

API

PropTypeDefaultDescription
label string '' Visible label rendered above the field.
help-text string '' Helper text shown below the field. Property: `helpText`.
error-text string '' Validation message below the field. Replaces help-text and reddens the border. Property: `errorText`.
hint string '' DEPRECATED — renamed to help-text. Still honoured, and warns in the console.
placeholder string 'Search or add...' Placeholder shown when no tokens are selected.
options EsaInputTagOption[] [] Optional suggestion list. Set as a property. When provided, a chevron toggles a filtered dropdown.
size 'xs' | 'sm' | 'md' | 'lg' 'md' Control size (shared form scale).
disabled boolean false Disables interaction and hides the remove buttons.
required boolean false Blocks submission while no token has been added (valueMissing), and renders an asterisk on the label. A half-typed term that was never committed does not count.
strict boolean false
tags-below boolean false Property: `tagsBelow`.
name string | undefined — Form field name — the key this control submits under.
value string[] | string | null — Selected tokens. Set/read as a property; mirrored to the form as a comma-joined string. Set as a JS property — there is no attribute.

Events

EventTypeDefaultDescription
change CustomEvent<{ value: string[] }> — Fired whenever tokens are added or removed. Composed and bubbling; detail.value is the current array of token values.
Binding these events from Angular

Every one of these is a CustomEvent, so the payload is on $event.detail — $event.target.value reads empty, with no error. The names are lowercase and Angular passes them to addEventListener verbatim, so camel-casing one means it never fires.

  • (change)="onChange($event.detail)"

Accessibility

  • The text field carries role="combobox" with aria-expanded, aria-haspopup="listbox", and aria-autocomplete="list".
  • The dropdown is role="listbox"; each suggestion and the "Add" row are role="option" with aria-selected.
  • Keyboard: Enter adds the typed value or the highlighted suggestion, ArrowUp/ArrowDown move through suggestions, Backspace on an empty field removes the last token, Escape closes the dropdown.
  • Each chip's remove button has an aria-label naming the token it removes; outside-click closes the dropdown.
  • Form-associated via ElementInternals — values submit with the enclosing form.

Theming surface

Every public token this component reads, extracted from its source at build time, split by blast radius: the hooks that belong to this component, then the shared and system tokens it consumes alongside everything else. The Resolves to column walks each token's real lineage — component → semantic → primitive → raw value — so you can see exactly where re-pointing it lands. A spoke re-skins by overriding these in its theme-<slug>.css; component internals are never edited. No hook for what you need? File it with /request-lego.

Type roles it names

This component does not set a size, weight, face or tracking of its own — it names a composite, and the composite carries all five. A composite is a class rather than a token, so none of it appears in the tables below; re-theme text by re-pointing the token family, not by editing the component.

RoleRe-pointReach
.typography-body-2xs
  • --typography-body-2xs-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-body-2xs-font-size clamp(0.5rem, 0.44rem + 0.3vw, 0.625rem) via --font-size-050
  • --typography-body-2xs-font-weight 350 via --typography-font-weight-regular → --font-weight-350
  • --typography-body-2xs-letter-spacing 0.01em via --letter-spacing-default
  • --typography-body-2xs-line-height 1.6 via --line-height-normal
+6 more components esa-checkbox, esa-checkbox-group, esa-combobox, esa-progress-bar, esa-radio-group, esa-textarea
.typography-body-lg
  • --typography-body-lg-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-body-lg-font-size clamp(0.875rem, 0.77rem + 0.52vw, 1.125rem) via --font-size-300
  • --typography-body-lg-font-weight 350 via --typography-font-weight-regular → --font-weight-350
  • --typography-body-lg-letter-spacing 0.01em via --letter-spacing-default
  • --typography-body-lg-line-height 1.8 via --line-height-relaxed
+7 more components esa-breadcrumbs, esa-checkbox, esa-checkbox-group, esa-combobox, esa-radio-group, esa-select, esa-textarea
.typography-body-md
  • --typography-body-md-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-body-md-font-size clamp(0.75rem, 0.66rem + 0.44vw, 0.9375rem) via --font-size-200
  • --typography-body-md-font-weight 350 via --typography-font-weight-regular → --font-weight-350
  • --typography-body-md-letter-spacing 0.01em via --letter-spacing-default
  • --typography-body-md-line-height 1.6 via --line-height-normal
+28 more components esa-app-shell, esa-breadcrumbs, esa-card, esa-checkbox, esa-checkbox-group, esa-collapsible, esa-combobox, esa-command-palette, esa-confirm-dialog, esa-container, esa-danger-zone, esa-dialog, esa-dropdown-menu, esa-empty-state, esa-entity-search, esa-form-field, esa-header-nav, esa-nav-dropdown, esa-page-header, esa-popover, esa-progress-bar, esa-radio-group, esa-select, esa-side-dialog, esa-snackbar-item, esa-switch-toggle, esa-tab-layout, esa-textarea
.typography-body-sm
  • --typography-body-sm-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-body-sm-font-size clamp(0.6875rem, 0.61rem + 0.38vw, 0.875rem) via --font-size-150
  • --typography-body-sm-font-weight 350 via --typography-font-weight-regular → --font-weight-350
  • --typography-body-sm-letter-spacing 0.01em via --letter-spacing-default
  • --typography-body-sm-line-height 1.6 via --line-height-normal
+20 more components esa-alert-box, esa-breadcrumbs, esa-button-toggle, esa-card, esa-checkbox-group, esa-combobox, esa-command-palette, esa-date-picker, esa-empty-state, esa-file-upload, esa-form-field, esa-link-column, esa-loading-overlay, esa-progress-bar, esa-radio-group, esa-search-panel, esa-select, esa-stat, esa-text-field, esa-textarea
.typography-body-xs
  • --typography-body-xs-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-body-xs-font-size clamp(0.625rem, 0.56rem + 0.32vw, 0.75rem) via --font-size-100
  • --typography-body-xs-font-weight 350 via --typography-font-weight-regular → --font-weight-350
  • --typography-body-xs-letter-spacing 0.01em via --letter-spacing-default
  • --typography-body-xs-line-height 1.6 via --line-height-normal
+14 more components esa-breadcrumbs, esa-checkbox, esa-checkbox-group, esa-combobox, esa-command-palette, esa-empty-state, esa-entity-search, esa-field-error, esa-header-nav, esa-progress-bar, esa-radio-group, esa-search-panel, esa-select, esa-textarea
.typography-label-2xs
  • --typography-label-2xs-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-label-2xs-font-size clamp(0.5rem, 0.44rem + 0.3vw, 0.625rem) via --font-size-050
  • --typography-label-2xs-font-weight 500 via --typography-font-weight-medium → --font-weight-500
  • --typography-label-2xs-letter-spacing 0.01em via --letter-spacing-default
  • --typography-label-2xs-line-height 1.6 via --line-height-normal
+11 more components esa-button-toggle, esa-checkbox-group, esa-color-picker, esa-combobox, esa-date-picker, esa-form-field, esa-progress-bar, esa-radio-group, esa-range-slider, esa-text-field, esa-textarea
.typography-label-lg
  • --typography-label-lg-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-label-lg-font-size clamp(0.875rem, 0.77rem + 0.52vw, 1.125rem) via --font-size-300
  • --typography-label-lg-font-weight 500 via --typography-font-weight-medium → --font-weight-500
  • --typography-label-lg-letter-spacing 0.01em via --letter-spacing-default
  • --typography-label-lg-line-height 1.6 via --line-height-normal
+12 more components esa-breadcrumbs, esa-button-toggle, esa-checkbox-group, esa-color-picker, esa-combobox, esa-date-picker, esa-form-field, esa-radio-group, esa-range-slider, esa-select, esa-text-field, esa-textarea
.typography-label-md
  • --typography-label-md-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-label-md-font-size clamp(0.75rem, 0.66rem + 0.44vw, 0.9375rem) via --font-size-200
  • --typography-label-md-font-weight 500 via --typography-font-weight-medium → --font-weight-500
  • --typography-label-md-letter-spacing 0.01em via --letter-spacing-default
  • --typography-label-md-line-height 1.6 via --line-height-normal
+33 more components esa-app-bar, esa-breadcrumbs, esa-button-group, esa-button-toggle, esa-card, esa-checkbox-group, esa-color-picker, esa-combobox, esa-command-palette, esa-confirm-dialog, esa-danger-zone, esa-date-picker, esa-dialog, esa-empty-state, esa-entity-search, esa-file-upload, esa-filter-container, esa-form-field, esa-header-nav, esa-link-column, esa-page-header, esa-pillbox, esa-popover, esa-progress-bar, esa-radio-group, esa-range-slider, esa-select, esa-side-dialog, esa-sidebar-nav, esa-stat, esa-text-field, esa-textarea, esa-tooltip
.typography-label-xs
  • --typography-label-xs-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-label-xs-font-size clamp(0.625rem, 0.56rem + 0.32vw, 0.75rem) via --font-size-100
  • --typography-label-xs-font-weight 500 via --typography-font-weight-medium → --font-weight-500
  • --typography-label-xs-letter-spacing 0.01em via --letter-spacing-default
  • --typography-label-xs-line-height 1.6 via --line-height-normal
+14 more components esa-breadcrumbs, esa-button-toggle, esa-checkbox-group, esa-color-picker, esa-combobox, esa-date-picker, esa-entity-search, esa-form-field, esa-progress-bar, esa-radio-group, esa-range-slider, esa-select, esa-text-field, esa-textarea
.typography-microcopy-2xs-subtle
  • --typography-microcopy-2xs-subtle-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-microcopy-2xs-subtle-font-size clamp(0.5rem, 0.44rem + 0.3vw, 0.625rem) via --font-size-050
  • --typography-microcopy-2xs-subtle-font-weight 350 via --typography-font-weight-regular → --font-weight-350
  • --typography-microcopy-2xs-subtle-letter-spacing 0.01em via --letter-spacing-default
  • --typography-microcopy-2xs-subtle-line-height 1 via --line-height-none
+3 more components esa-combobox, esa-date-picker, esa-text-field
.typography-microcopy-lg-subtle
  • --typography-microcopy-lg-subtle-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-microcopy-lg-subtle-font-size clamp(0.875rem, 0.77rem + 0.52vw, 1.125rem) via --font-size-300
  • --typography-microcopy-lg-subtle-font-weight 350 via --typography-font-weight-regular → --font-weight-350
  • --typography-microcopy-lg-subtle-letter-spacing 0.01em via --letter-spacing-default
  • --typography-microcopy-lg-subtle-line-height 1 via --line-height-none
+7 more components esa-combobox, esa-command-palette, esa-date-picker, esa-entity-search, esa-filter-dropdown, esa-select, esa-text-field
.typography-microcopy-md-subtle
  • --typography-microcopy-md-subtle-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-microcopy-md-subtle-font-size clamp(0.75rem, 0.66rem + 0.44vw, 0.9375rem) via --font-size-200
  • --typography-microcopy-md-subtle-font-weight 350 via --typography-font-weight-regular → --font-weight-350
  • --typography-microcopy-md-subtle-letter-spacing 0.01em via --letter-spacing-default
  • --typography-microcopy-md-subtle-line-height 1 via --line-height-none
+7 more components esa-combobox, esa-date-picker, esa-filter-dropdown, esa-pagination, esa-search-panel, esa-select, esa-text-field
.typography-microcopy-xs-subtle
  • --typography-microcopy-xs-subtle-font-family 'DM Sans', sans-serif via --typography-font-family-sans → --font-family-dm-sans
  • --typography-microcopy-xs-subtle-font-size clamp(0.625rem, 0.56rem + 0.32vw, 0.75rem) via --font-size-100
  • --typography-microcopy-xs-subtle-font-weight 350 via --typography-font-weight-regular → --font-weight-350
  • --typography-microcopy-xs-subtle-letter-spacing 0.01em via --letter-spacing-default
  • --typography-microcopy-xs-subtle-line-height 1 via --line-height-none
+7 more components esa-combobox, esa-date-picker, esa-filter-dropdown, esa-filter-pills, esa-select, esa-text-field, esa-tooltip

Wired to this component

None. This component has no hooks of its own — it themes entirely through the shared tokens below, so every override here also moves its siblings.

Shared tokens it reads

Owned by a family or by the semantic layer. Still overridable — but scope the override (esa-foo { --form-border-color: … }) unless you mean to move everything that reads it. Reach says what else that is.

Token Reach Resolves to
--focus-ring-color component focus ring group · +34 more #3e9b4f via --color-border-default-focus → --color-grass-10
--focus-ring-offset component focus ring group · +31 more 2px
--focus-ring-width component focus ring group · +35 more 2px via --border-width-focus → --border-width-200
--form-border-color component forms group · +11 more one role per property, so a theme can move the ring's weight and its colour independently. Tier 3's --focus-ring-width now reads this instead of holding a bare 2px, which is the same shape --focus-ring-color has always had (a thin tier-3 alias over a tier-2 role) — width was simply never given the tier-2 half. * * This role was rejected once, and the rejection is worth recording because its PREMISE was wrong rather than its reasoning. The argument against an `emphasis` width was that every 2px border in the kit is micro-geometry — spinner rings, the kbd keycap lip, a chevron. The focus ring was not in that enumeration and does not fit it: 33 components render it, more than read any other tier-3 token, and it is not micro-geometry but an accessibility affordance whose weight is a legitimate brand axis (a spoke that needs a heavier ring for contrast has to be able to reach it). It is also not a `border` in any component — every consumer paints it with `outline` or `box-shadow` — which is precisely why a survey of BORDERS missed it. The name still says `border` because tier 2 already calls the ring's colour --color-border-default-focus via --color-border-default
--form-border-color-focus component forms group · +11 more #3e9b4f via --color-border-default-focus → --color-grass-10
--form-border-color-hover component forms group · +3 more #bbbbbb via --color-border-default-strong → --color-gray-8
--form-border-width component forms group · +10 more 1px via --border-width-default → --border-width-100
--form-error-border-color component forms group · +9 more #ce2c31 via --color-background-utility-danger → --color-red-11
--form-error-color component forms group · +9 more #ce2c31 via --color-content-utility-danger → --color-red-11
--form-help-color component forms group · +9 more #646464 via --color-content-default-secondary → --color-gray-11
--form-label-color component forms group · +7 more #646464 via --color-content-default-secondary → --color-gray-11
--form-placeholder-color component forms group · +4 more #646464 via --color-content-default-secondary → --color-gray-11
--form-text-color component forms group · +6 more #202020 via --color-content-default → --color-gray-12
--color-background-disabled semantic every component #f0f0f0 via --color-gray-3
--color-background-elevation-raised semantic every component #fcfcfc via --color-gray-1
--color-background-elevation-sunken semantic every component #f0f0f0 via --color-gray-3
--color-background-field semantic every component rgba(0, 0, 0, 0)
--color-background-overlay-active semantic every component #46a758 via --color-background-brand → --color-grass-9
--color-background-overlay-strong-hover semantic every component rgba(0, 0, 0, 0.05) via --color-black-a-1
--color-border-default-focus semantic every component #3e9b4f via --color-grass-10
--color-border-default-subtle semantic every component #d9d9d9 via --color-gray-6
--color-border-disabled semantic every component #d9d9d9 via --color-gray-6
--color-content-brand semantic every component #2a7e3b via --color-grass-11
--color-content-default semantic every component #202020 via --color-gray-12
--color-content-default-muted semantic every component #646464 via --color-content-default-secondary → --color-gray-11
--color-content-default-secondary semantic every component #646464 via --color-gray-11
--color-content-disabled semantic every component #8d8d8d via --color-gray-9
--color-content-utility-danger semantic every component #ce2c31 via --color-red-11
--elevation-4 semantic every component 0 via --shadow-offset-x
--radius-md semantic every component 0.5rem via --radius-200
--radius-pill semantic every component 9999px via --radius-full
--radius-sm semantic every component 0.25rem via --radius-100
--transition-fast semantic every component 150ms via --duration-150
--z-dropdown semantic every component 50
--font-style-italic primitive every component italic
--icon-size-sm primitive every component 16px
--spacing-050 primitive every component 0.125rem
--spacing-100 primitive every component 0.25rem
--spacing-200 primitive every component 0.5rem
--spacing-250 primitive every component 0.625rem
--spacing-300 primitive every component 0.75rem
--spacing-400 primitive every component 1rem