tagsField
Free-form tags typed by editors. Stored as a comma-separated list, or as rows in a join table with auto-completion.
import { tagsField } from 'nextjs-cms/core/fields'Use tagsField when editors should be able to invent values ("nextjs", "tutorial", "2026"). If they must choose from a fixed or managed list, use selectMultipleField instead.
Basic usage
tagsField({ name: 'keywords' })Editors type a tag and press Enter or comma. Stored as nextjs,cms,tutorial in a LONGTEXT column.
With every option
tagsField({
name: 'tags',
label: { en: 'Tags', ar: 'الوسوم' },
required: false,
order: 7,
localized: true,
conditionalRules: [{ field: isPublic, condition: 'equals', value: true }],
placeholder: 'Add a tag and press Enter',
sanitize: true,
validation: {
regex: /^[a-z0-9-]{2,30}$/,
message: 'Tags use 2–30 lowercase letters, digits or hyphens.',
},
destinationDb: {
table: 'post_tags',
itemIdentifier: 'post_id',
selectIdentifier: 'tag',
},
autoCompletion: true, // requires destinationDb
})Options
| Option | Type | Default | Description |
|---|---|---|---|
placeholder | string | none | Hint in the empty input. |
sanitize | boolean | false | HTML-escape each tag before saving. |
validation | { regex: RegExp; message: string } | none | Every tag must match regex; otherwise the save fails with message. |
destinationDb | { table, itemIdentifier, selectIdentifier } | none | Store one row per tag in a join table. |
autoCompletion | boolean | false | Suggest existing tags while typing. Requires destinationDb. |
Plus the common options.
Options in detail
destinationDb and autoCompletion
Without a destination table, tags live in one text column and there's nothing to search for suggestions. With one, each tag is a row, and autoCompletion suggests tags already used by any item:
tagsField({
name: 'tags',
destinationDb: { table: 'post_tags', itemIdentifier: 'post_id', selectIdentifier: 'tag' },
autoCompletion: true,
})post_tags
post_id │ tag
────────┼───────────
12 │ nextjs
12 │ tutorial
15 │ nextjsautoCompletion without destinationDb is a TypeScript error and also throws at runtime.
validation
Checked per tag. Use it to keep tags consistent:
// Lowercase slugs only
validation: { regex: /^[a-z0-9]+(?:-[a-z0-9]+)*$/, message: 'Use lowercase words separated by hyphens.' }
// Hashtags
validation: { regex: /^#\w{2,}$/, message: 'Tags must start with # and have at least 2 characters.' }Tags can't contain commas: the comma separates tags, both in the input and in the stored value.
Examples
SEO keywords (simple)
tagsField({ name: 'keywords', placeholder: 'Keywords for search engines' })export async function generateMetadata({ params }) {
const post = await getPost((await params).slug)
return { keywords: post.keywords?.split(',') ?? [] }
}Tag pages on your website
import { eq } from 'drizzle-orm'
// All posts tagged "nextjs"
const posts = await db
.select({ post: PostsTable })
.from(PostTagsTable)
.innerJoin(PostsTable, eq(PostsTable.id, PostTagsTable.postId))
.where(eq(PostTagsTable.tag, 'nextjs'))
// Tag cloud with counts
import { count, desc } from 'drizzle-orm'
const cloud = await db
.select({ tag: PostTagsTable.tag, uses: count() })
.from(PostTagsTable)
.groupBy(PostTagsTable.tag)
.orderBy(desc(count()))Storage
| Mode | Column | Value |
|---|---|---|
| Default | LONGTEXT | Comma-separated tags |
destinationDb | none | One row per tag |