How to create an advanced tags input component with React
Tags: beginners, react, tutorial
Published: 2023-08-15 23:33:11

<img width="100%" style="width:100%" src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExOHBoeW56OTg3OGlraTNkczMxY2NubW02Nm1mZ3cwdW90a2VrbndvZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/wucPXPTQt8qpqQIVS3/giphy.gif">

There are enough good tutorials on how to build a tag input component in React JS. However, typically, it is a very simple component without functionality to search for existing tags (for example, from a database) and present them as options to choose from. Also, there was no tutorial on how to add functionality for colorizing tags.

In this tutorial, I will try to cover both of these topics.

## Project Setup
We are going to create a tags input component using React JS. To start, we will create a new React application using the Create React App (CRA) tool.

```
npx create-react-app tags-input
cd tags-input
```
Then, install the necessary dependencies. For this project we will use react-select, react-popper, and nanoid.

```
npm install react-select react-popper nanoid
```

Let's create two additional folders inside the src folder called "data" and "tags".

Inside the data folder, create two files: COLORS.js and TAGS.js.

```javascript
// COLORS.js
export const COLORS = [
    "#e93a55", "#f94e45", "#ff8549", "#3e993c", "#1e8a78",
    "#238cd7", "#6d65a8", "#414a53", "#e36dab", "#4abeb7",
    "#ff8657", "#ffb855", "#84c15f", "#00bd9d", "#00b2d7",
    "#967cd7", "#a8b2bc", '#fb5779', '#ff7511', '#ffa800',
    '#ffd100', '#ace60f', '#19db7e', '#00d4c8', '#48dafd',
    '#008ce3', '#6457f9', '#9f46e4', '#ff78ff', '#ff4ba6'
]
```

```javascript
// TAGS.js
export const TAGS = [
    { id: 1, value: "React",  label: "React", color: COLORS[1] },
    { id: 2, value: "Angular", label: "Angular", color: COLORS[2] },
    { id: 3, value: "Vue", label: "Vue", color: COLORS[3] },
    { id: 4, value: "Javascript", label: "Javascript", color: COLORS[4] },
    { id: 5, value: "Typescript", label: "Typescript", color: COLORS[5] }
]
```


## Let's start to work on tags component
Inside a tags folder create files called Tags.js. This component will render list of tags and tags input field.

```javascript
import { useCallback, useEffect, useRef, useState } from "react"
import { TAGS } from "../data/TAGS"
import { TagsList } from "./TagsList"
import { TagsInputField } from "./TagsInputField"

export const Tags = () => {

    const [tags, setTags] = useState(TAGS.slice(0, 3))

    return (
        <div className="TaskTags">
            <TagsList
                tags={tags}
                setTags={setTags}
            />

            <TagsInputField
                tags={tags}
                setTags={setTags}
            />
        </div>
    )
}
```

Than in the same folder create files TagsList.js, Tag.js and TagsInputField.js files.

```javascript
//TagsList.js
import { Tag } from "./Tag"

export const TagsList = ({
    tags,
    setTags
}) => {

    return (
        <>
            {tags.map((tag, index) => <Tag
                key={tag.id}
                tag={tag}
                setTags={setTags}
            />)}
        </>
    )
}
```

```javascript
// Tag.js
import { forwardRef } from "react"

export const Tag = forwardRef(({ tag, setTags }, ref) => {

    const removeTag = () => setTags(prevState => prevState.filter(i => i.id !== tag.id))

    return (
        <div
            ref={ref}
            className="TaskTag"
            style={{
                background: tag.color
            }}
        >
            <span className="TaskTag-label">{tag.label}</span>
            <svg onClick={removeTag} className="TaskTag-deleteButton" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
                <path fill="none" d="M0 0h24v24H0z"/>
                <path d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/>
            </svg>
        </div>
    )
})
```


## TagsInputField.js
In this component we are going to use async select component, that provide possibility to fetch from server existing tags and create new tags.

```javascript
import { forwardRef, useCallback, useEffect } from "react"
import { nanoid } from "nanoid"
import AsyncCreatableSelect from "react-select/async-creatable"
import { components } from "react-select"
import { TAGS } from "../data/TAGS"
import { COLORS } from "../data/COLORS"

export const TagsInputField = forwardRef(({
    tags,
    setTags
}, ref) => {

    const addTag = (value) => setTags(prevState => [...prevState, value])

    const handleCreateOption = async (value) => {
        const additionalOption = createOption(value)

        addTag(additionalOption)
    }

    // function to remove tags on Backspace key press
    const listener = useCallback((e) => {
        if (e.key === 'Backspace') {
            setTags(prev => prev.filter((_, i) => i !== tags.length - 1))
            if (ref.current) ref.current.focus()
        }

    }, [ref, tags, setTags])

    // add "keydown" event listener
    useEffect(() => {
        if (ref.current) ref.current.focus()

        document.addEventListener("keydown", listener)

        return () => {
            document.removeEventListener("keydown", listener)
        }
    }, [ref, listener])

    return (
        <AsyncCreatableSelect
            ref={ref}
            name="tags"
            value={{}}
            loadOptions={(value) => promiseOptions(value, tags)}
            menuPlacement={'auto'}
            components={{ LoadingIndicator: null, Option, SinleValue }}
            classNamePrefix="select"
            placeholder=""
            styles={tagsListStyles}
            cacheOptions
            onCreateOption={handleCreateOption}
            onChange={addTag}
        />
    )
})

// "fetch" options from "backend"
const promiseOptions = (inputValue, tags) =>
    new Promise((resolve) => {
        setTimeout(() => {
            resolve(filterTags(inputValue, tags))
        }, 1000)
    }
)

// filter already selected tags
// and filter tags from "backend" by input value
const filterTags = (inputValue, tags) => {
    return TAGS.filter(x => !tags.includes(x)).filter((i) =>
        i.label.toLowerCase().includes(inputValue.toLowerCase())
    )
}

const createOption = (value) => ({
    id: nanoid(),
    value,
    label: value,
    color: COLORS[16]
})

// And let's add some custom styling to react-select component
const SinleValue = (props) => {

    const { data } = props

    return (
        <components.SingleValue {...props}>
            <div className="Select-option">
                <span className="SelectColorSingleValue" style={{
                    backgroundColor: data.color
                }}>
                    {data.label}
                </span>
            </div>
        </components.SingleValue>
    )
}

const Option = (props) => {

    const {data} = props

    return (
        <components.Option {...props}>
            <div className="Select-option">
                <span>
                    {data.label}
                </span>
            </div>
        </components.Option>
    )
}

const tagsListStyles = ({
    container: (provided) => ({
        ...provided,
        width: "100%"
    }),

    control: (provided, state) => ({
        ...provided,
        height: 28,
        minHeight: 28,
        width: "100%",
        fontSize: 14,
        borderRadius: 6,
        padding: '0 0 0 8px',
        ':hover': {
            cursor: 'pointer'
        },
        borderColor: 'transparent',
        border: state.isFocused ? '1px solid transparent' : '1px solid #transparent',
        boxShadow: state.isFocused ? 0 : 0,
        boxSizing: 'border-box',
        '&:hover': {
            borderColor: state.isFocused ? 'transparent' : 'transparent',
            boxShadow: state.isFocused ? 0 : 0,
            boxSizing: 'border-box',
        }
    }),

    valueContainer: (provided) => ({
        ...provided,
        padding: 0
    }),

    indicatorSeparator: (provided) => ({
        ...provided,
        display: 'none'
    }),

    indicatorContainer: (provided) => ({
        ...provided,
        padding: 3
    }),

    dropdownIndicator: (provided) => ({
        ...provided,
        display: 'none'
    }),

    menu: (provided) => ({
        ...provided,
        boxShadow: '0 0 0 1px rgb(111 119 130 / 15%), 0 5px 20px 0 rgb(21 27 38 / 8%)',
        background: '#fff',
        width: "100%",
        boxSizing: 'border-box',
        margin: 0
    }),

    option: (provided, {isSelected}) => ({
        ...provided,
        color: "#151b26",
        fontSize: 14,
        minHeight: "36px",
        backgroundColor: isSelected && "#fff" ,
        ':hover': {
            cursor: 'pointer',
            backgroundColor: " #f2f6f8"
        }
    })
})

```

Now we can import and use in our App.js

```javascript
import './App.css'
import { Tags } from './tags/Tags'

function App() {

  return (
    <div className="App">
      <div className="Tags">
        <Tags />
      </div>
    </div>
  )
}

export default App
```

And also add these CSS styles in our src/App.css

```css
*{
    margin: 0;
    padding: 0;
}
html, body{
    height: 100%;
}
body{
    display: flex;
    justify-content: center;
    align-items: center;
    font-family: 'Courier New', Courier, monospace;
    font-weight: bold;
}

.App {
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    display: flex;
    flex-direction: column;
    overflow: hidden;
    position: absolute;
}

.Tags {
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    display: flex;
    flex-direction: column;
    position: absolute;
    justify-content: center;
    align-items: center;
}

.TaskTags {
    align-items: center;
    border: 1px solid #14aaf5;
    border-radius: 8px;
    display: flex;
    min-height: 38px;
    padding: 5px;
    display: flex;
    min-width: 1px;
    width: 700px;
    flex-wrap: wrap;
    max-width: 100%;
}

.css-fyq6mk-container {
    display: flex;
    flex: 1 1 auto;
    min-width: 180px;
    width: auto !important;
}

.Select-option {
    align-items: center;
    display: flex;
}

.PopoverReferenceElement {
    align-items: center;
    display: flex;
    flex: 1 1 auto;
}

.ActiveTaskData-content .PopoverReferenceElement {
    display: flex;
    flex: 0 0 auto;
}

.TaskTagWithColorSelect {
    align-items: center;
    display: flex;
}

.TaskTag {
    margin-bottom: 2px;
    margin-right: 4px;
    margin-top: 2px;
    display: flex;
    align-items: center;
    color: #fff;
    fill: #fff;
    border-radius: 10px;
    font-size: 12px;
    height: 20px;
    line-height: 20px;
    padding: 0 8px;
}

.TaskTag-label {
    text-overflow: ellipsis;
    box-sizing: border-box;
    display: block;
    font-weight: 400;
    max-width: 180px;
    overflow: hidden;
    text-align: left;
    white-space: nowrap;
    font-size: 12px;
    height: 20px;
    line-height: 20px;
    padding: 0 8px;
}

.TaskTag-deleteButton {
    border-radius: 10px;
    cursor: pointer;
    font-size: 12px;
    height: 12px;
    line-height: 20px;
    min-height: 12px;
    min-width: 12px;
    width: 12px;
}

.ColorSelectPopUp {
    align-items: center;
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 0 0 1px #e8ecee, 0 5px 20px 0 rgba(21, 7, 38, 0.08);
    box-sizing: border-box;
    color: #151b26;
    display: flex;
    justify-content: center;
    flex-wrap: wrap;
    padding: 10px;
    position: relative;
    width: 241px;
    z-index: 5000;
}

.ColorItem {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    border-width: 1px;
    border-color: #fff;
    border-style: solid;
    border-radius: 50%;
    background-color: #fff;
    cursor: pointer;
    padding: 2px;
    transition-property: border-color;
    transition-duration: 200ms;
}

.ColorItem:hover {
    border-color: #26d1b9;
    background-color: #fff;
}

.SelectedColorItem {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 22px;
    height: 22px;
    background-color: #fff;
    border-width: 1px;
    border-color: #26d1b9;
    border-style: solid;
    border-radius: 50%;
    background-color: #fff;
    cursor: pointer;
    padding: 2px;
    transition-property: border-color;
    transition-duration: 200ms;
}

.ColorIndicator {
    border-radius: 50%;
    height: 18px;
    width: 18px;
}

```

**We have finished with first part**
Now we can add existing tags from list, remove selected tags and create new tags. However all new tags will be grey color.


## Second topic: Function for tag color changing
And now we are goin to add possibility to select colors for all created tags.

## Tags colorizing
Let me briefly describe how it will work.
When we create new tag, we add it to tags list. Next, using react-popper library, we will show popup with list of available colors, where user may select color for tag.
If user start typing new tag we simply hide popup.


Popup element should be displayed near created tag, it means we have to set last tag in the list as "referenceElement".
However tags list is an array of elements and if we want to refer to one of it's elements we need to save to the ref array of elements: `ref={el => tagsRef.current[index] = el}`

First of all let's modify Tags.js component: add reference element, popper element, state to show popper and tagsRef (which is an empty array).

Additionally we need to add the state to store id of active tag for which we will change color.

And finally add outsideClickHandler to hide popup.

```javascript
import { useCallback, useEffect, useRef, useState } from "react"
import { TAGS } from "../data/TAGS"
import { TagsList } from "./TagsList"
import { TagsInputField } from "./TagsInputField"

export const Tags = () => {

    const [tags, setTags] = useState(TAGS.slice(0, 3))
    
    const [referenceElement, setReferenceElement] = useState(null)
    const [popperElement, setPopperElement] = useState(null)
    const [showPopper, setShowPopper] = useState(false)


    const [activeTagId, setActiveTagId] = useState(null)

    const tagsRef = useRef([])

    useEffect(() => {
        if (activeTagId === null) return

        if (tags.indexOf(tags.find(tag => tag.id === activeTagId)) >= 0) {
            setReferenceElement(tagsRef.current[tags.indexOf(tags.find(tag => tag.id === activeTagId))])
            setShowPopper(true)
        }
    }, [setShowPopper, tags, tagsRef, activeTagId])

    const selectRef = useRef(null)

    const outsideClickHandler = useCallback((event) => {
        if (!popperElement || popperElement.contains(event.target)) {
            return
        }
        if (popperElement && !popperElement.contains(event.target)) {
            setShowPopper(false)
            setActiveTagId(null)
        }
    }, [popperElement, setShowPopper, setActiveTagId])

    useEffect(() => {
        if (popperElement) {
            document.addEventListener("mousedown", outsideClickHandler)
            return () => {
                document.removeEventListener("mousedown", outsideClickHandler)
            }
        }
    }, [popperElement, outsideClickHandler])

    return (
        <div className="TaskTags">
            <TagsList
                referenceElement={referenceElement}
                popperElement={popperElement}
                selectRef={selectRef}
                activeTagId={activeTagId}
                tags={tags}
                tagsRef={tagsRef}
                showPopper={showPopper}
                setPopperElement={setPopperElement}
                setTags={setTags}
            />

            <TagsInputField
                ref={selectRef}
                tags={tags}
                setTags={setTags}
                setActiveTagId={setActiveTagId}
                setShowPopper={setShowPopper}
            />
        </div>
    )
}

```

Next we are goin to modify TagsInput.js: set active tag id when adding new tag and hide popup when user starts typing new tag title in input field.

```javascript
import { forwardRef, useCallback, useEffect } from "react"
import { nanoid } from "nanoid"
import AsyncCreatableSelect from "react-select/async-creatable"
import { components } from "react-select"
import { TAGS } from "../data/TAGS"
import { COLORS } from "../data/COLORS"

export const TagsInputField = forwardRef(({
    tags,
    setTags,
    setActiveTagId,
    setShowPopper
}, ref) => {

    const addTag = (value) => setTags(prevState => [...prevState, value])

    const handleCreateOption = async (value) => {
        const additionalOption = createOption(value)

        addTag(additionalOption)

        setActiveTagId(additionalOption.id)
    }

    const listener = useCallback((e) => {
        if (e.key === 'Backspace') {
            setTags(prev => prev.filter((_, i) => i !== tags.length - 1))
            if (ref.current) ref.current.focus()
        }

        setShowPopper(false)
        setActiveTagId(null)
    }, [ref, tags, setTags, setShowPopper, setActiveTagId])

    useEffect(() => {
        if (ref.current) ref.current.focus()

        document.addEventListener("keydown", listener)

        return () => {
            document.removeEventListener("keydown", listener)
        }
    }, [ref, listener])

// other code remains without changes
```

## React tags input component COMPLETED! :tada:

{% embed https://codesandbox.io/embed/sad-rain-ztt4c7?fontsize=14&hidenavigation=1&theme=dark %}