Filter chip

Allow the user to enter information, filter content, and make selections.

Props

error
boolean
Shows an error state.
Defaults to false.
content
string
Text label of the chip.
secondarytext
string
Secondary text displayed in a smaller size before the main content.
leadingIcon
GoAIconType
Icon displayed at the start of the chip.
testId
string
Sets a data-testid attribute for automated testing.
ariaLabel
string
Accessible label for the filter chip. Defaults to content with 'removable' suffix.
mt, mr, mb, ml
none | 3xs | 2xs | xs | s | m | l | xl | 2xl | 3xl | 4xl
Apply margin to the top, right, bottom, and/or left of the component.

Events

onClick
(event: Event) => void
_click
CustomEvent
Examples

Add a filter chip

const [activeFilters, setActiveFilters] = useState<string[]>([]);

const addFilter = () => {
  const randomFilter = `Filter ${Math.floor(Math.random() * 100)}`;
  if (!activeFilters.includes(randomFilter)) {
    setActiveFilters((prevFilters) => [...prevFilters, randomFilter]);
  }
};

const removeFilter = (filter: string) => {
  setActiveFilters((prevFilters) => prevFilters.filter((f) => f !== filter));
};
<div>
  {activeFilters.map((filter) => (
    <GoabFilterChip
      key={filter}
      content={filter}
      onClick={() => removeFilter(filter)}
      mr="s"
      mb="s"
      mt="s"
    />
  ))}
</div>
<GoabButton mt="l" onClick={addFilter}>
  Add Random Filter
</GoabButton>
activeFilters: string[] = [];

removeFilter(filter: string): void {
  this.activeFilters = this.activeFilters.filter((f) => f !== filter);
}

addFilter(): void {
  const randomFilter = "Filter " + Math.floor(Math.random() * 100);
  if (!this.activeFilters.includes(randomFilter)) {
    this.activeFilters.push(randomFilter);
  }
}
<div>
  @for (filter of activeFilters; track filter) {
  <goab-filter-chip
    [content]="filter"
    (onClick)="removeFilter(filter)"
    mr="s"
    mb="s"
    mt="s"
  >
  </goab-filter-chip>
  }
</div>
<goab-button mt="l" (onClick)="addFilter()">Add Random Filter</goab-button>
const activeFilters = [];
const container = document.getElementById("filter-container");
const addBtn = document.getElementById("add-filter-btn");

function renderFilters() {
  container.innerHTML = "";
  activeFilters.forEach((filter) => {
    const chip = document.createElement("goa-filter-chip");
    chip.setAttribute("version", "2");
    chip.setAttribute("content", filter);
    chip.setAttribute("mr", "s");
    chip.setAttribute("mb", "s");
    chip.setAttribute("mt", "s");
    chip.addEventListener("_click", () => removeFilter(filter));
    container.appendChild(chip);
  });
}

function addFilter() {
  const randomFilter = "Filter " + Math.floor(Math.random() * 100);
  if (!activeFilters.includes(randomFilter)) {
    activeFilters.push(randomFilter);
    renderFilters();
  }
}

function removeFilter(filter) {
  const index = activeFilters.indexOf(filter);
  if (index > -1) {
    activeFilters.splice(index, 1);
    renderFilters();
  }
}

addBtn.addEventListener("_click", addFilter);
<div id="filter-container"></div>
<goa-button version="2" mt="l" id="add-filter-btn">Add Random Filter</goa-button>

Filter data in a table

const [typedChips, setTypedChips] = useState<string[]>([]);
const [inputValue, setInputValue] = useState("");
const [inputError, setInputError] = useState("");
const errorEmpty = "Empty filter";
const errorDuplicate = "Enter a unique filter";

const data = useMemo(
  () => [
    {
      status: { type: "information" as GoabBadgeType, text: "In progress" },
      name: "Ivan Schmidt",
      id: "7838576954",
    },
    {
      status: { type: "success" as GoabBadgeType, text: "Completed" },
      name: "Luz Lakin",
      id: "8576953364",
    },
    {
      status: { type: "information" as GoabBadgeType, text: "In progress" },
      name: "Keith McGlynn",
      id: "9846041345",
    },
    {
      status: { type: "success" as GoabBadgeType, text: "Completed" },
      name: "Melody Frami",
      id: "7385256175",
    },
    {
      status: { type: "important" as GoabBadgeType, text: "Updated" },
      name: "Frederick Skiles",
      id: "5807570418",
    },
    {
      status: { type: "success" as GoabBadgeType, text: "Completed" },
      name: "Dana Pfannerstill",
      id: "5736306857",
    },
  ],
  [],
);

const [dataFiltered, setDataFiltered] = useState(data);

const handleInputChange = (detail: GoabInputOnChangeDetail) => {
  const newValue = detail.value.trim();
  setInputValue(newValue);
};

const handleInputKeyPress = (detail: GoabInputOnKeyPressDetail) => {
  if (detail.key === "Enter") {
    applyFilter();
  }
};

const applyFilter = () => {
  if (inputValue === "") {
    setInputError(errorEmpty);
    return;
  }
  if (typedChips.length > 0 && typedChips.includes(inputValue)) {
    setInputError(errorDuplicate);
    return;
  }
  setTypedChips([...typedChips, inputValue]);
  setTimeout(() => {
    setInputValue("");
  }, 0);
  setInputError("");
};

const removeTypedChip = (chip: string) => {
  setTypedChips(typedChips.filter((c) => c !== chip));
  setInputError("");
};

const checkNested = useCallback((obj: object, chip: string): boolean => {
  return Object.values(obj).some((value) =>
    typeof value === "object" && value !== null
      ? checkNested(value, chip)
      : typeof value === "string" && value.toLowerCase().includes(chip.toLowerCase()),
  );
}, []);

const getFilteredData = useCallback(
  (typedChips: string[]) => {
    if (typedChips.length === 0) {
      return data;
    }
    return data.filter((item: object) =>
      typedChips.every((chip) => checkNested(item, chip)),
    );
  },
  [checkNested, data],
);

useEffect(() => {
  setDataFiltered(getFilteredData(typedChips));
}, [getFilteredData, typedChips]);
<GoabFormItem id="filterChipInput" error={inputError} mb="m">
  <GoabBlock gap="xs" direction="row" alignment="start" width="100%">
    <div style={{ flex: 1 }}>
      <GoabInput
        name="filterChipInput"
        aria-labelledby="filterChipInput"
        value={inputValue}
        leadingIcon="search"
        width="100%"
        onChange={handleInputChange}
        onKeyPress={handleInputKeyPress}
      />
    </div>
    <GoabButton type="secondary" onClick={applyFilter} leadingIcon="filter">
      Filter
    </GoabButton>
  </GoabBlock>
</GoabFormItem>

{typedChips.length > 0 && (
  <div>
    <GoabText tag="span" color="secondary" mb="xs" mr="xs">
      Filter:
    </GoabText>
    {typedChips.map((typedChip, index) => (
      <GoabFilterChip
        key={index}
        content={typedChip}
        mb="xs"
        mr="xs"
        onClick={() => removeTypedChip(typedChip)}
      />
    ))}
    <GoabButton
      type="tertiary"
      size="compact"
      mb="xs"
      onClick={() => setTypedChips([])}
    >
      Clear all
    </GoabButton>
  </div>
)}

<GoabTable width="100%">
  <thead>
    <tr>
      <th>Status</th>
      <th>Name</th>
      <th className="goa-table-number-header">ID Number</th>
    </tr>
  </thead>
  <tbody>
    {dataFiltered.map((item) => (
      <tr key={item.id}>
        <td>
          <GoabBadge
            type={item.status.type}
            content={item.status.text}
            icon={false}
          />
        </td>
        <td>{item.name}</td>
        <td className="goa-table-number-column">{item.id}</td>
      </tr>
    ))}
  </tbody>
</GoabTable>

{dataFiltered.length === 0 && data.length > 0 && (
  <GoabBlock mt="l" mb="l">
    No results found
  </GoabBlock>
)}
typedChips: string[] = [];
inputValue = "";
inputError = "";
readonly errorEmpty = "Empty filter";
readonly errorDuplicate = "Enter a unique filter";

readonly data: DataItem[] = [
  {
    status: { type: "information", text: "In progress" },
    name: "Ivan Schmidt",
    id: "7838576954",
  },
  {
    status: { type: "success", text: "Completed" },
    name: "Luz Lakin",
    id: "8576953364",
  },
  {
    status: { type: "information", text: "In progress" },
    name: "Keith McGlynn",
    id: "9846041345",
  },
  {
    status: { type: "success", text: "Completed" },
    name: "Melody Frami",
    id: "7385256175",
  },
  {
    status: { type: "important", text: "Updated" },
    name: "Frederick Skiles",
    id: "5807570418",
  },
  {
    status: { type: "success", text: "Completed" },
    name: "Dana Pfannerstill",
    id: "5736306857",
  },
];

dataFiltered = this.getFilteredData(this.typedChips);

handleInputChange(detail: GoabInputOnChangeDetail): void {
  const newValue = detail.value.trim();
  this.inputValue = newValue;
}

handleInputKeyPress(detail: GoabInputOnKeyPressDetail): void {
  if (detail.key === "Enter") {
    this.applyFilter();
  }
}

applyFilter(): void {
  if (this.inputValue === "") {
    this.inputError = this.errorEmpty;
    return;
  }
  if (this.typedChips.includes(this.inputValue)) {
    this.inputError = this.errorDuplicate;
    return;
  }
  this.typedChips = [...this.typedChips, this.inputValue];
  this.inputValue = "";
  this.inputError = "";
  this.dataFiltered = this.getFilteredData(this.typedChips);
}

removeTypedChip(chip: string): void {
  this.typedChips = this.typedChips.filter((c) => c !== chip);
  this.dataFiltered = this.getFilteredData(this.typedChips);
  this.inputError = "";
}

removeAllTypedChips(): void {
  this.typedChips = [];
  this.dataFiltered = this.getFilteredData(this.typedChips);
  this.inputError = "";
}

getFilteredData(typedChips: string[]): DataItem[] {
  if (typedChips.length === 0) {
    return this.data;
  }
  return this.data.filter((item) =>
    typedChips.every((chip) => this.checkNested(item, chip)),
  );
}

checkNested(obj: object, chip: string): boolean {
  return Object.values(obj).some((value) =>
    typeof value === "object" && value !== null
      ? this.checkNested(value, chip)
      : typeof value === "string" && value.toLowerCase().includes(chip.toLowerCase()),
  );
}
<goab-form-item id="filterChipInput" [error]="inputError" mb="m">
  <goab-block gap="xs" direction="row" alignment="start" width="100%">
    <div style="flex: 1">
      <goab-input
        name="filterChipInput"
        aria-labelledby="filterChipInput"
        [value]="inputValue"
        leadingIcon="search"
        width="100%"
        (onChange)="handleInputChange($event)"
        (onKeyPress)="handleInputKeyPress($event)"
      >
      </goab-input>
    </div>
    <goab-button type="secondary" (onClick)="applyFilter()" leadingIcon="filter">
      Filter
    </goab-button>
  </goab-block>
</goab-form-item>

@if (typedChips.length > 0) {
<ng-container>
  <goab-text tag="span" color="secondary" mb="xs" mr="xs"> Filter: </goab-text>
  @for (typedChip of typedChips; track typedChip; let index = $index) {
  <goab-filter-chip
    [content]="typedChip"
    mb="xs"
    mr="xs"
    (onClick)="removeTypedChip(typedChip)"
  >
  </goab-filter-chip>
  }
  <goab-button type="tertiary" size="compact" mb="xs" (onClick)="removeAllTypedChips()">
    Clear all
  </goab-button>
</ng-container>
}

<goab-table width="100%">
  <thead>
    <tr>
      <th>Status</th>
      <th>Name</th>
      <th class="goa-table-number-header">ID Number</th>
    </tr>
  </thead>
  <tbody>
    @for (item of dataFiltered; track $index) {
    <tr>
      <td>
        <goab-badge
          [type]="item.status.type"
          [content]="item.status.text"
          [icon]="false"
        ></goab-badge>
      </td>
      <td>{{ item.name }}</td>
      <td class="goa-table-number-column">{{ item.id }}</td>
    </tr>
    }
  </tbody>
</goab-table>

@if (dataFiltered.length === 0 && data.length > 0) {
<goab-block mt="l" mb="l"> No results found </goab-block>
}
const filterInput = document.getElementById("filter-input");
const filterBtn = document.getElementById("filter-btn");
const filterFormItem = document.getElementById("filter-form-item");
const chipsContainer = document.getElementById("chips-container");
const chipsList = document.getElementById("chips-list");
const clearAllBtn = document.getElementById("clear-all-btn");
const tableRows = document.querySelectorAll("tbody tr");

let typedChips = [];

function filterTable() {
  tableRows.forEach((row) => {
    const badge = row.querySelector("goa-badge");
    const badgeText = badge ? badge.getAttribute("content") || "" : "";
    const text = (row.textContent + " " + badgeText).toLowerCase();
    const matches =
      typedChips.length === 0 ||
      typedChips.every((chip) => text.includes(chip.toLowerCase()));
    row.style.display = matches ? "" : "none";
  });
}

function renderChips() {
  chipsList.innerHTML = "";
  typedChips.forEach((chip) => {
    const filterChip = document.createElement("goa-filter-chip");
    filterChip.setAttribute("version", "2");
    filterChip.setAttribute("content", chip);
    filterChip.setAttribute("mb", "xs");
    filterChip.setAttribute("mr", "xs");
    filterChip.addEventListener("_click", () => removeChip(chip));
    chipsList.appendChild(filterChip);
  });
  chipsContainer.style.display = typedChips.length > 0 ? "block" : "none";
  filterTable();
}

function applyFilter() {
  const value = filterInput.value.trim();
  if (value === "") {
    filterFormItem.setAttribute("error", "Empty filter");
    return;
  }
  if (typedChips.includes(value)) {
    filterFormItem.setAttribute("error", "Enter a unique filter");
    return;
  }
  typedChips.push(value);
  filterInput.value = "";
  filterFormItem.removeAttribute("error");
  renderChips();
}

function removeChip(chip) {
  typedChips = typedChips.filter((c) => c !== chip);
  renderChips();
}

filterBtn.addEventListener("_click", applyFilter);
clearAllBtn.addEventListener("_click", () => {
  typedChips = [];
  renderChips();
});
<goa-form-item version="2" id="filter-form-item" mb="m">
  <goa-block gap="xs" direction="row" alignment="center" width="100%">
    <div style="flex: 1">
      <goa-input
        version="2"
        id="filter-input"
        name="filterChipInput"
        leadingicon="search"
        width="100%"
      >
      </goa-input>
    </div>
    <goa-button version="2" id="filter-btn" type="secondary" leadingicon="filter">
      Filter
    </goa-button>
  </goa-block>
</goa-form-item>

<div id="chips-container" style="display: none">
  <goa-text as="span" color="secondary" mb="xs" mr="xs">Filter:</goa-text>
  <span id="chips-list"></span>
  <goa-button version="2" id="clear-all-btn" type="tertiary" size="compact" mb="xs">
    Clear all
  </goa-button>
</div>

<goa-table version="2" width="100%" mt="s">
  <table style="width: 100%">
    <thead>
      <tr>
        <th>Status</th>
        <th>Name</th>
        <th class="goa-table-number-header">ID Number</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td>
          <goa-badge
            version="2"
            type="information"
            content="In progress"
            icon="false"
          ></goa-badge>
        </td>
        <td>Ivan Schmidt</td>
        <td class="goa-table-number-column">7838576954</td>
      </tr>
      <tr>
        <td>
          <goa-badge
            version="2"
            type="success"
            content="Completed"
            icon="false"
          ></goa-badge>
        </td>
        <td>Luz Lakin</td>
        <td class="goa-table-number-column">8576953364</td>
      </tr>
      <tr>
        <td>
          <goa-badge
            version="2"
            type="information"
            content="In progress"
            icon="false"
          ></goa-badge>
        </td>
        <td>Keith McGlynn</td>
        <td class="goa-table-number-column">9846041345</td>
      </tr>
      <tr>
        <td>
          <goa-badge
            version="2"
            type="success"
            content="Completed"
            icon="false"
          ></goa-badge>
        </td>
        <td>Melody Frami</td>
        <td class="goa-table-number-column">7385256175</td>
      </tr>
      <tr>
        <td>
          <goa-badge
            version="2"
            type="important"
            content="Updated"
            icon="false"
          ></goa-badge>
        </td>
        <td>Frederick Skiles</td>
        <td class="goa-table-number-column">5807570418</td>
      </tr>
      <tr>
        <td>
          <goa-badge
            version="2"
            type="success"
            content="Completed"
            icon="false"
          ></goa-badge>
        </td>
        <td>Dana Pfannerstill</td>
        <td class="goa-table-number-column">5736306857</td>
      </tr>
    </tbody>
  </table>
</goa-table>

Remove a filter

const [chips, setChips] = useState(["Chip 1", "Chip 2", "Chip 3"]);

const deleteChip = (chip: string) => {
  setChips((prevChips) => prevChips.filter((c) => c !== chip));
};
<div>
  {chips.map((chip) => (
    <GoabFilterChip
      key={chip}
      content={chip}
      onClick={() => deleteChip(chip)}
      mr="s"
    />
  ))}
</div>
chips: string[] = ["Chip 1", "Chip 2", "Chip 3"];

deleteChip(chip: string): void {
  this.chips = this.chips.filter((c) => c !== chip);
}
<div>
  @for (chip of chips; track chip) {
  <goab-filter-chip [content]="chip" (onClick)="deleteChip(chip)" mr="s">
  </goab-filter-chip>
  }
</div>
let chips = ["Chip 1", "Chip 2", "Chip 3"];
const container = document.getElementById("chip-container");

function renderChips() {
  container.innerHTML = "";
  chips.forEach((chip) => {
    const chipEl = document.createElement("goa-filter-chip");
    chipEl.setAttribute("version", "2");
    chipEl.setAttribute("content", chip);
    chipEl.setAttribute("mr", "s");
    chipEl.addEventListener("_click", () => deleteChip(chip));
    container.appendChild(chipEl);
  });
}

function deleteChip(chip) {
  chips = chips.filter((c) => c !== chip);
  renderChips();
}

renderChips();
<div id="chip-container"></div>

Type to create a new filter

const [typedChips, setTypedChips] = useState<string[]>([]);
const [inputValue, setInputValue] = useState("");

const addChip = () => {
  if (inputValue.trim()) {
    setTypedChips([...typedChips, inputValue.trim()]);
    setInputValue("");
  }
};
<GoabFormItem label="Type to create a chip" mb="m">
  <GoabBlock gap="xs" direction="row">
    <div style={{ flex: 1 }}>
      <GoabInput
        name="chipInput"
        value={inputValue}
        onChange={(e) => setInputValue(e.value)}
        onKeyPress={(e) => {
          if (e.key === "Enter" && e.value.trim()) {
            setTypedChips([...typedChips, e.value.trim()]);
            setTimeout(() => setInputValue(""), 0);
          } else if (
            e.key === "Backspace" &&
            !e.value.trim() &&
            typedChips.length > 0
          ) {
            setTypedChips(typedChips.slice(0, -1));
          }
        }}
        width="100%"
      />
    </div>
    <GoabButton type="secondary" onClick={addChip}>
      Add
    </GoabButton>
  </GoabBlock>
</GoabFormItem>
<div>
  {typedChips.map((chip, index) => (
    <GoabFilterChip
      key={index}
      content={chip}
      mb="xs"
      mr="xs"
      onClick={() => setTypedChips(typedChips.filter((c) => c !== chip))}
    />
  ))}
</div>
typedChips: string[] = [];
inputValue = "";

handleInputChange(detail: GoabInputOnChangeDetail): void {
  const newValue = detail.value.trim();
  this.inputValue = newValue;
}

handleInputKeyPress(detail: GoabInputOnKeyPressDetail): void {
  const newValue = detail.value.trim();
  if (detail.key === "Enter" && newValue !== "") {
    this.addChip();
  } else if (
    !this.inputValue &&
    this.typedChips.length > 0 &&
    detail.key === "Backspace"
  ) {
    this.typedChips.pop();
  }
}

addChip(): void {
  if (this.inputValue.trim()) {
    this.typedChips.push(this.inputValue.trim());
    this.inputValue = "";
  }
}

removeTypedChip(chip: string): void {
  this.typedChips = this.typedChips.filter((c) => c !== chip);
}
<goab-form-item label="Type to create a chip" mb="m">
  <goab-block gap="xs" direction="row">
    <div style="flex: 1">
      <goab-input
        [value]="inputValue"
        (onChange)="handleInputChange($event)"
        (onKeyPress)="handleInputKeyPress($event)"
        width="100%"
      >
      </goab-input>
    </div>
    <goab-button type="secondary" (onClick)="addChip()">Add</goab-button>
  </goab-block>
</goab-form-item>
@if (typedChips.length > 0) {
<div>
  @for (typedChip of typedChips; track typedChip) {
  <goab-filter-chip
    [content]="typedChip"
    mb="xs"
    mr="xs"
    (onClick)="removeTypedChip(typedChip)"
  >
  </goab-filter-chip>
  }
</div>
}
const typedChips = [];
const input = document.getElementById("chip-input");
const addBtn = document.getElementById("add-btn");
const container = document.getElementById("chips-container");
let currentValue = "";

function renderChips() {
  container.innerHTML = "";
  typedChips.forEach((chip) => {
    const chipEl = document.createElement("goa-filter-chip");
    chipEl.setAttribute("version", "2");
    chipEl.setAttribute("content", chip);
    chipEl.setAttribute("mb", "xs");
    chipEl.setAttribute("mr", "xs");
    chipEl.addEventListener("_click", () => removeChip(chip));
    container.appendChild(chipEl);
  });
}

function addChip() {
  if (currentValue.trim()) {
    typedChips.push(currentValue.trim());
    currentValue = "";
    input.setAttribute("value", "");
    renderChips();
  }
}

function removeChip(chip) {
  const index = typedChips.indexOf(chip);
  if (index > -1) {
    typedChips.splice(index, 1);
    renderChips();
  }
}

input.addEventListener("_change", (e) => {
  currentValue = e.detail.value || "";
});

addBtn.addEventListener("_click", () => {
  addChip();
});
<goa-form-item version="2" label="Type to create a chip" mb="m">
  <goa-block gap="xs" direction="row">
    <div style="flex: 1">
      <goa-input version="2" id="chip-input" width="100%"></goa-input>
    </div>
    <goa-button version="2" id="add-btn" type="secondary">Add</goa-button>
  </goa-block>
</goa-form-item>
<div id="chips-container"></div>

Types

FilterChip is for removable filters that users can dismiss. For static labels or status indicators, use Badge instead.
All GoA Design System components are built to meet WCAG 2.2 AA standards. The following guidelines provide additional context for accessible implementation.

No accessibility-specific guidelines have been documented for this component yet.

View old component docs