TagsInput
TagsInput
자유 입력과 제안(suggestions)으로 사용자로부터 값 목록을 받는 컴포넌트예요. 여러 값을 태그 형태로 입력받을 때 사용해요.
출처: 문서
본문
Combobox 기반 (Made with Combobox)
TagsInput은 Combobox 컴포넌트 위에 만들어진 고정된(opinionated) 컴포넌트예요. 기본 사용 사례만 다루는 제한된 기능을 가져요. 더 고급 기능이 필요하면 Combobox로 직접 컴포넌트를 만들 수 있어요. 커스텀 태그 입력 컴포넌트 예시는 예시 페이지에서 찾을 수 있어요.
사용법 (Usage)
TagsInput은 여러 값을 입력하는 방법을 제공해요. 제안과 함께 또는 제안 없이 사용할 수 있어요. TagsInput은 MultiSelect와 비슷하지만 커스텀 값을 입력할 수 있어요.
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<TagsInput
label="Press Enter to submit a tag"
placeholder="Enter tag"
data={['React', 'Angular', 'Svelte']}
/>
);
}
로딩 상태 (Loading state)
loading prop을 설정하면 로딩 인디케이터를 표시해요. 기본적으로 로더는 입력 오른쪽에 표시돼요. loadingPosition prop을 'left' 또는 'right'로 설정해 위치를 바꿀 수 있어요. 이는 API 호출, 검색, 검증 같은 비동기 작업에 유용해요.
import { TagsInput } from '@mantine/core';
function Demo() {
return <TagsInput data={[]} loading placeholder="Loading" />;
}
제어 컴포넌트 (Controlled)
TagsInput의 값은 문자열 배열이어야 해요. 다른 타입은 지원되지 않아요. onChange 함수는 문자열 배열을 단일 인자로 호출돼요.
import { useState } from 'react';
import { TagsInput } from '@mantine/core';
function Demo() {
const [value, setValue] = useState<string[]>([]);
return <TagsInput data={[]} value={value} onChange={setValue} />;
}
제어된 검색 값 (Controlled search value)
searchValue와 onSearchChange props로 검색 값을 제어할 수 있어요.
import { useState } from 'react';
import { TagsInput } from '@mantine/core';
function Demo() {
const [searchValue, setSearchValue] = useState('');
return (
<TagsInput searchValue={searchValue} onSearchChange={setSearchValue} />
);
}
지우기 가능 (Clearable)
clearable prop을 설정하면 오른쪽 섹션에 지우기 버튼을 표시해요. 버튼은 다음 경우에 표시되지 않아요.
- 컴포넌트에 값이 없을 때
- 컴포넌트가 비활성(disabled)일 때
- 컴포넌트가 읽기 전용(read only)일 때
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<TagsInput
label="Press Enter to submit a tag"
placeholder="Enter tag"
defaultValue={['React']}
clearable
/>
);
}
지우기 섹션 모드 (Clear section mode)
clearSectionMode prop은 지우기 버튼과 rightSection이 어떻게 렌더링되는지 결정해요.
'both'(기본) – 지우기 버튼과rightSection모두 렌더링해요'rightSection'– 사용자가 제공한rightSection만 렌더링하고 지우기 버튼을 무시해요'clear'– 지우기 버튼만 렌더링하고rightSection을 무시해요
import { CaretDownIcon } from '@phosphor-icons/react';
import { Stack, TagsInput } from '@mantine/core';
function Demo() {
return (
<Stack>
<TagsInput data={['React']} defaultValue={['React']} rightSection={<CaretDownIcon size={16} />} clearSectionMode="both" />
<TagsInput data={['React']} defaultValue={['React']} rightSection={<CaretDownIcon size={16} />} clearSectionMode="rightSection" />
<TagsInput data={['React']} defaultValue={['React']} rightSection={<CaretDownIcon size={16} />} clearSectionMode="clear" />
</Stack>
);
}
최대 선택 값 (Max selected values)
maxTags prop으로 선택할 수 있는 값의 개수를 제한할 수 있어요. 한도에 도달하면 더 이상 값을 추가할 수 없어요.
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<TagsInput
label="Press Enter to submit a tag"
description="Add up to 3 tags"
placeholder="Enter tag"
defaultValue={['first', 'second']}
maxTags={3}
/>
);
}
블러 시 값 수락 (Accept value on blur)
기본적으로 사용자가 값을 입력하고 입력란을 벗어나면(blur) 값이 목록에 추가돼요. acceptValueOnBlur를 false로 설정하면 이 동작을 바꿀 수 있어요. 이 경우 사용자가 Enter를 누르거나 제안을 클릭할 때만 값이 추가돼요.
중복 허용 (Allow duplicates)
기본적으로 TagsInput은 중복 값 추가를 허용하지 않지만, allowDuplicates prop을 설정하면 이 동작을 바꿀 수 있어요. 값이 value 배열에 이미 있으면 대소문자와 끝 공백과 관계없이 중복으로 간주돼요.
isDuplicate
isDuplicate prop으로 중복 감지 방식을 제어할 수 있어요. 이 함수는 태그 값과 현재 태그 두 인자를 받아요. 값이 중복이면 true를 반환해야 해요.
같은 값을 다른 대소문자로 사용할 수 있게 isDuplicate를 사용하는 예시:
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<TagsInput
isDuplicate={(tagValue, currentTags) => currentTags.some((val) => val === tagValue)}
defaultValue={['Tag', 'TAG', 'tag']}
/>
);
}
분할 문자 (Split chars)
기본적으로 TagsInput은 값들을 쉼표(,)로 분할해요. splitChars prop을 문자열 배열로 설정하면 이 동작을 바꿀 수 있어요. splitChars의 모든 값은 최종 값에 포함될 수 없어요. 값은 붙여넣기(paste) 시에도 분할돼요.
,, |, 공백으로 분할하는 예시:
import { TagsInput } from '@mantine/core';
function Demo() {
return <TagsInput splitChars={[',', '|', ' ']} label="Press Enter to submit a tag" />;
}
제안과 함께 (With suggestions)
TagsInput은 제안과 함께 사용할 수 있어요. 입력 아래에 제안 목록을 렌더링하고 키보드나 마우스로 제안을 선택할 수 있게 해요. 사용자가 제안에 제한되지 않고 커스텀 값을 입력할 수 있다는 점에 주의해요. 제안의 값만 허용하려면 MultiSelect 컴포넌트를 대신 사용해요.
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<TagsInput
data={['React', 'Angular', 'Svelte', 'Vue']}
label="Press Enter to submit a tag"
placeholder="Enter tag"
/>
);
}
데이터 형식 (Data formats)
TagsInput의 data prop은 다음 형식 중 하나의 데이터를 받아요.
문자열 배열:
import { TagsInput } from '@mantine/core';
function Demo() {
return <TagsInput data={['React', 'Angular', 'Svelte']} />;
}
문자열 옵션이 있는 그룹 배열:
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<TagsInput
data={[
{ group: 'Frontend', items: ['React', 'Angular'] },
{ group: 'Backend', items: ['Node.js', 'Python'] },
]}
/>
);
}
옵션 필터링 (Options filtering)
기본적으로 TagsInput은 옵션 라벨이 입력 값을 포함하는지 확인해 옵션을 필터링해요. filter prop으로 이 동작을 바꿀 수 있어요. filter 함수는 다음 속성이 있는 객체를 단일 인자로 받아요.
options– 옵션 또는 옵션 그룹 배열, 모든 옵션은{ value: string; label: string; disabled?: boolean }형식search– 현재 검색어limit–TagsInput에 전달된limitprop의 값
글자 시퀀스 대신 단어로 옵션을 매칭하는 커스텀 필터 함수 예시:
import { TagsInput, ComboboxItem, OptionsFilter } from '@mantine/core';
const optionsFilter: OptionsFilter = ({ options, search }) => {
const splittedSearch = search.toLowerCase().trim().split(' ');
return (options as ComboboxItem[]).filter((option) => {
const words = option.label.toLowerCase().trim().split(' ');
return splittedSearch.every((searchWord) => words.some((word) => word.includes(searchWord)));
});
};
function Demo() {
return (
<TagsInput
label="What countries have you visited?"
placeholder="Pick countries that you have visited"
data={['USA', 'Canada', 'Mexico']}
filter={optionsFilter}
/>
);
}
옵션 정렬 (Sort options)
기본적으로 옵션은 data 배열에서의 위치 순서로 정렬돼요. filter 함수로 이 동작을 바꿀 수 있어요.
import { TagsInput, ComboboxItem, OptionsFilter } from '@mantine/core';
const optionsFilter: OptionsFilter = ({ options, search }) => {
const filtered = (options as ComboboxItem[]).filter((option) =>
option.label.toLowerCase().trim().includes(search.toLowerCase().trim())
);
filtered.sort((a, b) => a.label.localeCompare(b.label));
return filtered;
};
fuse.js로 퍼지 검색 (Fuzzy search with fuse.js)
fuse.js 라이브러리를 사용해 오타나 부분 일치에도 옵션을 매칭하는 퍼지 검색을 구현할 수 있어요.
import { TagsInput, ComboboxItem, OptionsFilter } from '@mantine/core';
import Fuse from 'fuse.js';
const optionsFilter: OptionsFilter = ({ options, search }) => {
if (!search.trim()) {
return options;
}
const fuse = new Fuse(options as ComboboxItem[], {
keys: ['label'],
threshold: 0.3,
minMatchCharLength: 1,
});
return fuse.search(search).map((result) => result.item);
};
대규모 데이터셋 (Large data sets)
대규모 데이터셋에 대한 최선의 전략은 동시에 렌더링되는 옵션 수를 제한하는 거예요. limit prop으로 할 수 있어요. 커스텀 filter 함수를 사용한다면 filter 안에서 옵션 수를 제한하는 직접 로직을 구현해야 한다는 점에 주의해요.
100,000개 옵션이 있는 TagsInput 예시로, 동시에 5개 옵션만 렌더링돼요.
import { TagsInput } from '@mantine/core';
const largeData = Array(100_000)
.fill(0)
.map((_, index) => `Option ${index}`);
function Demo() {
return <TagsInput data={largeData} limit={5} />;
}
renderOption
renderOption 콜백으로 옵션 렌더링을 커스터마이즈할 수 있어요. 옵션 객체로 호출돼요. 함수는 React 노드를 반환해야 해요.
import { Group, TagsInput, TagsInputProps, Text } from '@mantine/core';
const data: Record<string, { emoji: string; description: string }> = {
Apples: { emoji: '🍎', description: 'Crisp and juicy snacking delight' },
Bread: { emoji: '🍞', description: 'Freshly baked daily essential' },
Bananas: { emoji: '🍌', description: 'Perfect for a healthy breakfast' },
Eggs: { emoji: '🥚', description: 'Versatile protein source for cooking' },
Broccoli: { emoji: '🥦', description: 'Nutrient-rich green vegetable' },
};
const renderTagsInputOption: TagsInputProps['renderOption'] = ({ option }) => (
<Group>
<Text>{data[option.value].emoji}</Text>
<div>
<Text>{option.value}</Text>
<Text size="xs" opacity={0.5}>{data[option.value].description}</Text>
</div>
</Group>
);
필 커스터마이즈 (Pill customization)
renderPill 콜백으로 필(pill) 렌더링을 커스터마이즈할 수 있어요. option(combobox item), value(문자열), onRemove(함수), disabled를 포함하는 객체로 호출돼요. TagsInput은 커스텀 값을 추가할 수 있으므로 option 속성은 즉석에서 생성될 수 있다는 점에 주의해요.
import { TagsInput, Pill } from '@mantine/core';
function Demo() {
return (
<TagsInput
data={['React', 'Angular']}
label="Custom pills"
description="Tags are rendered with a star prefix"
defaultValue={['React', 'Angular']}
renderPill={({ value }) => <Pill>★ {value}</Pill>}
/>
);
}
필 재정렬 (Reorder pills)
withPillsReorder prop을 설정하면 필을 재정렬할 수 있어요. 다른 필 앞이나 뒤에 필을 놓으면(drop) 컴포넌트 값이 그에 따라 갱신돼요. disabled 또는 readOnly가 설정되면 재정렬은 자동으로 비활성화돼요.
필은 마우스(드래그 앤 드롭)나 키보드로 재정렬할 수 있어요.
- 필은
Tab순서에 포함되지 않아요. 입력에 포커스가 있는 상태에서 (입력의 시작 부분에 커서가 있을 때)ArrowLeft를 누르면 마지막 필로 포커스가 이동해요 ArrowLeft와ArrowRight는 필 사이에서 포커스를 이동해요(RTL 인식). 마지막 필에서ArrowRight를 누르면 입력으로 포커스가 돌아가요Alt + ArrowLeft와Alt + ArrowRight는 포커스된 필을 재정렬해요(RTL 인식)
포커스가 이동한 필을 따라가므로 다시 포커스하지 않고도 여러 이동을 연속으로 할 수 있어요.
import { useState } from 'react';
import { TagsInput } from '@mantine/core';
function Demo() {
const [value, setValue] = useState(['first', 'second', 'third']);
return <TagsInput value={value} onChange={setValue} withPillsReorder />;
}
renderPill prop으로 커스텀 필 렌더러를 사용한다면, 렌더 콜백 페이로드에서 reorderProps를 포커스 가능한 필 루트 요소에 펼쳐 재정렬이 계속 동작하게 해요. reorderProps는 키보드 재정렬을 구동하는 tabIndex, data-mantine-pill-index 속성, 키보드 핸들러를 담고 있어서, 사용자가 포커스할 수 있는 요소에 위치해야 해요.
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<TagsInput
withPillsReorder
renderPill={({ value, reorderProps }) => (
<span {...reorderProps}>{value} ×</span>
)}
/>
);
}
스크롤 가능한 드롭다운 (Scrollable dropdown)
기본적으로 옵션 목록은 ScrollArea.Autosize로 감싸져요. 기본 설정을 바꾸지 않았다면 maxDropdownHeight prop으로 드롭다운 최대 높이를 제어할 수 있어요.
네이티브 스크롤바를 사용하려면 withScrollArea={false}를 설정해요. 이 경우 Styles API로 드롭다운 스타일을 변경해야 한다는 점에 주의해요.
import { TagsInput } from '@mantine/core';
const data = Array(100)
.fill(0)
.map((_, index) => `Option ${index}`);
function Demo() {
return (
<>
<TagsInput data={data} label="With scroll area (default)" />
<TagsInput data={data} label="With native scroll" withScrollArea={false} />
</>
);
}
뷰포트 높이에 맞는 드롭다운 (Fit dropdown to viewport height)
floatingHeight="viewport"을 설정하면 드롭다운이 뷰포트의 사용 가능한 세로 공간을 채우도록 커져요. 이 모드에서는 flip 미들웨어가 비활성화돼요. 드롭다운이 항상 설정된 방향으로 열리고, 반대쪽으로 뒤집히는 대신 뷰포트 가장자리로 제한돼요. 큰 옵션 목록을 다룰 때 유용해요.
import { TagsInput } from '@mantine/core';
const data = Array(100)
.fill(0)
.map((_, index) => `Option ${index}`);
function Demo() {
return <TagsInput data={data} floatingHeight="viewport" />;
}
그룹 옵션 (Group options)
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<TagsInput
data={[
{ group: 'Frontend', items: ['React', 'Angular'] },
{ group: 'Backend', items: ['Node.js', 'Python'] },
]}
label="Enter tags"
placeholder="Enter tag"
/>
);
}
비활성 옵션 (Disabled options)
옵션이 비활성이면 선택할 수 없고 키보드 내비게이션에서 무시돼요. 사용자가 비활성 옵션을 값으로 입력할 수는 있다는 점에 주의해요. 특정 값을 금지하려면 제어 컴포넌트를 사용하고 onChange 함수에서 필터링해요.
Combobox props
Combobox props를 comboboxProps로 덮어쓸 수 있어요. TagsInput이 노출하지 않는 일부 props(예: withinPortal)를 변경해야 할 때 유용해요.
import { TagsInput } from '@mantine/core';
function Demo() {
return <TagsInput data={[]} comboboxProps={{ withinPortal: false }} />;
}
드롭다운 z-index 변경 (Change dropdown z-index)
import { TagsInput } from '@mantine/core';
function Demo() {
return <TagsInput data={[]} comboboxProps={{ zIndex: 1000 }} />;
}
Popover 안에서 (Inside Popover)
TagsInput를 popover 안에서 사용하려면 withinPortal: false를 설정해야 해요.
import { Popover, Button, TagsInput } from '@mantine/core';
function Demo() {
return (
<Popover width={400} position="bottom" withArrow shadow="md">
<Popover.Target>
<Button>Toggle popover</Button>
</Popover.Target>
<Popover.Dropdown>
<TagsInput data={[]} withinPortal={false} label="Enter tags" />
</Popover.Dropdown>
</Popover>
);
}
드롭다운 열림 상태 제어 (Control dropdown opened state)
dropdownOpened prop으로 드롭다운 열림 상태를 제어할 수 있어요. 또한 onDropdownClose와 onDropdownOpen으로 드롭다운 열림 상태 변경을 들을 수 있어요.
import { TagsInput, Button } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [dropdownOpened, { toggle }] = useDisclosure();
return (
<>
<Button onClick={toggle}>Toggle dropdown</Button>
<TagsInput
label="Your favorite library"
data={['React', 'Angular', 'Svelte']}
dropdownOpened={dropdownOpened}
/>
</>
);
}
드롭다운 위치 (Dropdown position)
기본적으로 드롭다운은 공간이 충분하면 입력 아래에 표시되고, 그렇지 않으면 입력 위에 표시돼요. position과 middlewares props를 설정해 이 동작을 바꿀 수 있어요. 이 props는 기반 Popover 컴포넌트로 전달돼요.
항상 입력 위에 표시되는 드롭다운 예시:
import { TagsInput } from '@mantine/core';
function Demo() {
return <TagsInput label="Your favorite library" data={['React']} position="top" />;
}
드롭다운 애니메이션 (Dropdown animation)
기본적으로 드롭다운 애니메이션은 비활성화돼 있어요. transitionProps를 설정해 활성화할 수 있으며, 이 props는 기반 Transition 컴포넌트로 전달돼요.
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<TagsInput
label="Your favorite library"
data={['React']}
transitionProps={{ transition: 'pop', duration: 200 }}
/>
);
}
드롭다운 너비 (Dropdown width)
드롭다운 너비를 바꾸려면 comboboxProps에서 width prop을 설정해요. 기본적으로 드롭다운 너비는 입력 너비와 같아요.
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<TagsInput label="Your favorite library" data={['React']} comboboxProps={{ width: 400 }} />
);
}
드롭다운 패딩 (Dropdown padding)
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<>
<TagsInput label="Zero padding" data={['React']} comboboxProps={{ dropdownPadding: 0 }} />
<TagsInput label="10px padding" data={['React']} comboboxProps={{ dropdownPadding: 10 }} />
</>
);
}
드롭다운 그림자 (Dropdown shadow)
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<TagsInput label="Your favorite library" data={['React']} comboboxProps={{ shadow: 'md' }} />
);
}
왼쪽·오른쪽 섹션 (Left and right sections)
TagsInput는 leftSection과 rightSection props를 지원해요. 이 섹션들은 입력 래퍼 안에 절대 위치로 렌더링돼요. 아이콘, 입력 컨트롤, 기타 요소를 표시하는 데 사용할 수 있어요.
rightSection/leftSection– 입력의 해당 측면에 렌더링할 React 노드rightSectionWidth/leftSectionWidth– 오른쪽 섹션의 너비와 입력 해당 측면의 패딩을 제어해요. 기본적으로 컴포넌트sizeprop으로 제어돼요.rightSectionPointerEvents/leftSectionPointerEvents– 섹션의pointer-events속성을 제어해요. 비상호작용 요소를 렌더링하려면none으로 설정해 클릭이 입력으로 전달되게 해요.
import { TagsInput } from '@mantine/core';
import { SquaresFourIcon } from '@phosphor-icons/react';
function Demo() {
const icon = <SquaresFourIcon size={16} />;
return (
<>
<TagsInput data={[]} label="Your favorite library" leftSection={icon} />
<TagsInput data={[]} label="Your favorite library" rightSection={icon} />
</>
);
}
Input props
TagsInput 컴포넌트는 Input과 Input.Wrapper 컴포넌트의 기능과 모든 input 요소 props를 지원해요. TagsInput 문서에는 컴포넌트가 지원하는 모든 기능이 포함되어 있지 않아요. 사용 가능한 모든 기능은 Input 문서에서 확인할 수 있어요.
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<TagsInput
label="Input label"
description="Input description"
data={['First', 'Second']}
defaultValue={['First', 'Second']}
placeholder="Enter tags"
/>
);
}
읽기 전용 (Read only)
readOnly를 설정하면 입력을 읽기 전용으로 만들어요. readOnly가 설정되면 TagsInput은 제안을 표시하지 않고 onChange 함수를 호출하지 않아요.
비활성 (Disabled)
disabled를 설정하면 입력을 비활성화해요. disabled가 설정되면 사용자가 입력과 상호작용할 수 없고 TagsInput은 제안을 표시하지 않아요.
오류 상태 (Error state)
불리언 오류 또는 오류 메시지와 함께 TagsInput을 표시할 수 있어요.
import { TagsInput } from '@mantine/core';
function Demo() {
return (
<>
<TagsInput label="Boolean error" data={[]} defaultValue={['React', 'Angular']} error />
<TagsInput label="With error message" data={[]} defaultValue={['React', 'Angular']} error="Invalid name" />
</>
);
}
성공 상태 (Success state)
import { TagsInput } from '@mantine/core';
function Demo() {
return <TagsInput label="Tags Input" description="Looks good!" data={[]} />;
}
Styles API
TagsInput는 Styles API를 지원해요. classNames prop으로 컴포넌트의 내부 요소에 스타일을 추가할 수 있어요. 자세한 내용은 Styles API 문서를 참고해요.
| selector | 설명 |
|---|---|
| wrapper | Input의 루트 요소 |
| input | Input 요소 |
| section | 왼쪽 및 오른쪽 섹션 |
| bottomSection | 하단 섹션 요소, 입력 테두리 안쪽 하단에 렌더링돼요 |
| root | 루트 요소 |
| label | 라벨 요소 |
| required | 필수 별표 요소, 라벨 안에 렌더링돼요 |
| description | 설명 요소 |
| error | 오류 요소 |
| success | 성공 요소 |
| dropdown | 드롭다운 루트 요소 |
| options | 옵션 래퍼 |
| option | 옵션 |
| empty | 아무것도 찾지 못했을 때의 메시지 |
| group | 옵션 그룹 래퍼 |
| groupLabel | 옵션 그룹 라벨 |
| pill | 값 필 |
| inputField | 입력 필드 |
| pillsList | 필 목록, 입력 필드도 포함해요 |
요소 ref 가져오기 (Get element ref)
import { useRef } from 'react';
import { TagsInput } from '@mantine/core';
function Demo() {
const ref = useRef<HTMLInputElement>(null);
return <TagsInput data={[]} ref={ref} />;
}
접근성 (Accessibility)
TagsInput을 label prop 없이 사용하면 화면 판독기가 제대로 알려주지 못해요. aria-label을 설정하면 라벨이 보이지 않아도 화면 판독기가 알려줘요. label prop을 설정하면 별도로 aria-label을 지정할 필요 없이 접근성이 확보돼요.
지우기 버튼에 aria-label을 설정하려면 clearButtonProps를 사용해요. 이는 clearable이 설정된 경우에만 필요하다는 점에 주의해요.
import { TagsInput } from '@mantine/core';
function Demo() {
return <TagsInput data={[]} clearable clearButtonProps={{ 'aria-label': 'Clear tags' }} />;
}