Gradio custom HTML component: Tag Input
Author: gradio
Category: input
Tags: input, tags, text
Description: Add and remove tags with keyboard input

Head:

HTML template:
<div class="tag-input-container">
    <div class="tag-list">
        ${(value || []).map((tag, i) => `
            <span class="tag-pill">
                ${tag}
                <button class="tag-remove" data-index="${i}">&times;</button>
            </span>
        `).join('')}
        <input type="text" class="tag-text-input" placeholder="${(value || []).length === 0 ? 'Type a tag and press Enter...' : 'Add more...'}" />
    </div>
</div>

CSS:
.tag-input-container { padding: 4px 0; }
.tag-list { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; padding: 8px; border: 1px solid #e5e7eb; border-radius: 8px; min-height: 42px; }
.tag-pill { display: inline-flex; align-items: center; gap: 4px; padding: 4px 10px; background: #fff7ed; color: #ea580c; border-radius: 16px; font-size: 13px; font-weight: 500; border: 1px solid #fed7aa; }
.tag-remove { background: none; border: none; color: #ea580c; cursor: pointer; font-size: 16px; padding: 0 2px; line-height: 1; opacity: 0.6; }
.tag-remove:hover { opacity: 1; }
.tag-text-input { border: none; outline: none; flex: 1; min-width: 120px; font-size: 14px; padding: 4px; background: transparent; }

JavaScript (on load):
const input = element.querySelector('.tag-text-input');
input.addEventListener('keydown', (e) => {
    if (e.key === 'Enter' && input.value.trim()) {
        e.preventDefault();
        const tags = [...(props.value || []), input.value.trim()];
        props.value = tags;
        input.value = '';
    }
    if (e.key === 'Backspace' && !input.value && props.value && props.value.length > 0) {
        props.value = props.value.slice(0, -1);
    }
});
element.addEventListener('click', (e) => {
    const btn = e.target.closest('.tag-remove');
    if (!btn) return;
    const idx = parseInt(btn.dataset.index);
    const tags = [...(props.value || [])];
    tags.splice(idx, 1);
    props.value = tags;
});

Python code:
class TagInput(gr.HTML):
    def __init__(self, value=None, **kwargs):
        html_template = """
        <div class="tag-input-container">
            <div class="tag-list">
                ${(value || []).map((tag, i) => `
                    <span class="tag-pill">
                        ${tag}
                        <button class="tag-remove" data-index="${i}">&times;</button>
                    </span>
                `).join('')}
                <input type="text" class="tag-text-input"
                       placeholder="Type a tag and press Enter..." />
            </div>
        </div>
        """
        css_template = """
        .tag-list {
            display: flex; flex-wrap: wrap; gap: 6px;
            padding: 8px; border: 1px solid #e5e7eb;
            border-radius: 8px;
        }
        .tag-pill {
            padding: 4px 10px; background: #fff7ed;
            color: #ea580c; border-radius: 16px;
            font-size: 13px; border: 1px solid #fed7aa;
        }
        """
        js_on_load = """
        const input = element.querySelector('.tag-text-input');
        input.addEventListener('keydown', (e) => {
            if (e.key === 'Enter' && input.value.trim()) {
                e.preventDefault();
                props.value = [...(props.value || []), input.value.trim()];
                input.value = '';
            }
        });
        element.addEventListener('click', (e) => {
            const btn = e.target.closest('.tag-remove');
            if (!btn) return;
            const idx = parseInt(btn.dataset.index);
            const tags = [...(props.value || [])];
            tags.splice(idx, 1);
            props.value = tags;
        });
        """
        super().__init__(
            value=value or [],
            html_template=html_template,
            css_template=css_template,
            js_on_load=js_on_load, **kwargs
        )

    def api_info(self):
        return {"type": "array", "items": {"type": "string"}}