Tags & Labels System
If you're building B2B SaaS in 2026 with content / records / projects / tickets, users expect tags or labels — flexible categorization without forcing rigid hierarchies. Notion, Linear, GitHub, Trello, Gmail, Asana all have them. The naive approach: comma-separated string field. The structured approach: many-to-many tags table, color-coded UI, tag autocomplete + create-on-the-fly, filtering / search by tag, tag analytics, deletion + cascade rules. Tags look simple from outside; the data model and UI patterns require deliberate design. This guide covers the implementation craft.
1. Decide tag model — flat, hierarchical, or scoped
Decide tag model.
Flat tags (most common):
- Simple list: "urgent", "bug", "feature", "design"
- No structure
- Used by: Linear, GitHub Issues, Trello
- Pro: simplicity
- Con: no relationships
Hierarchical tags (rare):
- Nested: "design > color > primary"
- Pro: organization
- Con: complex UX; rarely used well
- Used by: some content tools (Drupal taxonomies)
Scoped tags (recommended for many B2B SaaS):
- Per-resource-type: "Project tags" vs "Customer tags" vs "Document tags"
- Each scope has its own list
- Used by: Notion (per-database tags), Salesforce (per-object)
- Pro: less clutter
- Con: tag duplication across scopes
Workspace-wide tags vs per-list:
- Workspace-wide: shared across all resources in workspace
- Per-list: each list has its own (Trello-style)
- Hybrid: workspace shared, per-list extended
Categories vs tags:
- Categories: structured, exclusive (one project belongs to one category)
- Tags: free-form, multiple
- Use both? Or just tags?
For [USE CASE], output:
1. Recommended model
2. Scope decision (workspace / per-list / per-resource)
3. Tag character limits + format
4. Migration path if changing models
5. Multi-tenant considerations
The 2026 default for B2B SaaS: flat workspace-scoped tags with optional per-resource-type scoping. Notion / Linear pattern.
2. Schema design
Design tags schema.
Core tables:
tags table:
- id (UUID)
- workspace_id (multi-tenant scope)
- name (string, unique within workspace)
- slug (URL-safe; auto-generated from name)
- color (hex / token reference)
- description (optional)
- created_by_user_id
- created_at, updated_at
- deleted_at (soft delete)
resource_tags (many-to-many):
- tag_id (fk)
- resource_type (enum: 'project', 'task', 'customer', etc.)
- resource_id (uuid)
- tagged_by_user_id
- tagged_at
Indexes:
- (workspace_id, slug) unique
- (resource_type, resource_id) for fetching tags on a resource
- (tag_id) for fetching resources with tag
Optional fields:
- emoji (lightweight icon)
- is_system (created automatically; can't delete)
- group / category (if grouping)
- order (manual sort within group)
Polymorphic relations:
- resource_id + resource_type (Rails / Django pattern)
- Or: separate join tables per resource type (project_tags, customer_tags)
- Both work; polymorphic simpler; per-type more queryable
Tag scopes (if scoped):
- Add scope_type / scope_id to tags table
- e.g., scope_type='project'; tags only apply to projects
Migration considerations:
- From string field: parse + dedupe + create
- From CSV column: split + create tags
Output:
1. Schema (DDL)
2. Polymorphic vs per-type decision
3. Indexes
4. Soft-delete policy
5. Migration from existing data
The polymorphic-vs-per-type debate: polymorphic (single resource_tags table) is simpler. Per-type (project_tags, customer_tags) gives type-safety + indexes. For most B2B SaaS, polymorphic is fine.
3. Tag input UI — combobox with create-on-the-fly
Implement tag input UI.
Pattern: combobox
User experience:
- Click tag area → input opens
- Type to filter existing tags
- See suggestions
- Click to add OR
- Press Enter to create new (if doesn't exist)
- Press Escape to close
- Click X on tag to remove
Components:
- shadcn/ui Combobox + Badge
- Or: cmdk + custom badges
- Or: react-select for full feature
- Headless: Downshift, react-aria
Typeahead behavior:
- Search prefix-match (or fuzzy)
- Sort by recently-used + alphabetical
- "+Create '[tag]'" option at bottom for new
Multi-select:
- Allow multiple tags per resource
- Display as pills / badges
- Each removable via X
Create-on-the-fly:
- If user types tag that doesn't exist + presses Enter
- Show "Create tag 'foo'"
- On click: API call to create + add to resource
Validation:
- Min 1 char
- Max 30-50 chars (longer becomes ugly UI)
- No special characters except spaces, hyphens, underscores
- Or: emoji allowed (modern apps)
Color assignment:
- New tags: random color from palette
- User can edit color later in tag management
Mobile:
- Tap to open
- Native keyboard
- Pills with X on touch
Output:
1. Combobox component
2. Search behavior (prefix vs fuzzy)
3. Create-on-the-fly UX
4. Validation rules
5. Mobile pattern
The "create-on-the-fly" pattern is critical: forcing users to manage tags separately before applying them is friction. Type → see suggestions → create new if needed → done.
4. Color coding
Tags without color are harder to scan. Plan colors.
Implement tag colors.
Palette:
Core palette (8-12 colors typical):
- Red, Orange, Yellow, Green, Teal, Blue, Indigo, Purple, Pink, Gray, Brown, Black
Color tokens (Tailwind):
- bg-red-100 + text-red-800 (light theme)
- bg-red-900 + text-red-200 (dark theme)
- Both modes from one config
Color generation:
Auto (recommended for most):
- Hash tag name → color from palette
- Consistent: same tag name = same color across tag instances
- No user choice needed
- Example: hash('bug') → 'red'
User-chosen:
- Pick from palette
- More control
- Used by: Notion, Linear
Hybrid:
- Default to hashed
- User can override
Display:
- Light background + dark text (light mode)
- Dark background + light text (dark mode)
- Border (subtle) for definition
- Rounded full or rounded-md (style choice)
Accessibility:
- 4.5:1 contrast ratio (text on tag background)
- Colorblind-safe palette (avoid red/green-only differentiation)
- Tag name still readable; never color-only meaning
Anti-patterns:
- Too many colors (10+ feels chaotic)
- Inconsistent colors across views
- Color-only signals (colorblind issue)
Output:
1. Color palette
2. Auto vs user-chosen
3. Light / dark mode tokens
4. Accessibility check
5. Border / rounding style
The "auto-color from name hash" pattern: consistent + no user decision needed. If consistency matters less, allow user override.
5. Tag management page
Beyond tagging on resources, users need to manage tags overall.
Build tag management page.
Location:
- Settings → Tags (workspace)
- Or: dedicated tags page in nav (heavy tag use)
Features:
List all tags:
- Name, color, count of tagged resources, created date
- Sortable / searchable
Create tag:
- "New tag" button
- Modal: name + color + description
- Save → tag available for tagging
Edit tag:
- Inline edit name + color
- Cascade: all tagged resources reflect change
Delete tag:
- Confirm modal
- Options: "Delete + remove from N resources" OR "Just delete tag (untag resources)"
- Or: archive (soft delete)
Merge tags:
- Common ask (duplicate "bug" / "bugs")
- Select 2+ tags → merge into one
- All resources with merged tags now have target tag
Permissions:
- Anyone can apply tags? (open)
- Only admins can create / delete? (controlled)
- Workspace setting
Search:
- Find tag by name
- Filter by usage (most/least used)
Bulk operations:
- Multi-select tags
- Bulk delete / merge / change color
Anti-patterns:
- No tag management page (users can't fix tag chaos)
- Delete without "what happens to resources" prompt
- No merge feature (duplicates accumulate)
Output:
1. Page layout
2. CRUD operations
3. Cascade rules
4. Permission model
5. Bulk operations
The merge-tags feature: most-requested by power users. Tag chaos accumulates; merge is the cleanup tool.
6. Filtering by tags
Users filter their content by tag. Plan it.
Implement tag filtering.
Patterns:
Single tag filter:
- Click tag pill → filter to resources with that tag
- URL: ?tag=bug
Multi-tag filter:
- Multi-select tag dropdown
- AND logic: resources have ALL selected tags
- OR logic: resources have ANY selected tag (some products)
- URL: ?tags=bug,urgent
Combined with other filters:
- Tag + status + assignee + date
- Saved views / saved filters
UI patterns:
Tag chips above list:
- Show active tag filters as removable chips
- Click X to remove
Tag dropdown in filter bar:
- Click "Tags" → dropdown with checkboxes
- Apply
Inline tag filter (Linear-style):
- Click tag on resource → filter to that tag
- Quick way to discover related items
Search syntax:
- "tag:bug" or "label:urgent" in search input
- Power-user feature
Empty states:
- Filtered to nothing: "No items with tag 'bug'"
- "Clear filter" link
Performance:
- Index resource_tags by tag_id + resource_type
- Pagination still works
- Server-side filter (don't client-filter large lists)
Output:
1. Filter UI pattern
2. AND vs OR logic
3. URL state
4. Empty states
5. Performance considerations
The Linear pattern: click any tag anywhere → filters list to that tag. Fast tag-based exploration.
7. Tag analytics
Understanding which tags are used informs product decisions.
Track tag analytics.
Per-tag metrics:
- Usage count (how many resources tagged)
- Recency (last used)
- Trend (growing / stagnant)
Workspace-level:
- Total unique tags
- Tag tag-cloud visualization
- Most-used tags
- Unused tags (cleanup opportunity)
User-level:
- User's most-used tags
- Suggest in autocomplete
Reports:
- Cross-tag analysis (tags X + Y together)
- Tag → resource type distribution
Optimization:
- Suggest related tags (collaborative filtering)
- Auto-tag suggestions based on content (ML)
- Detect duplicates ("bug" vs "bugs")
UI:
- Tag management page shows usage count
- Tag analytics dashboard (admin)
Anti-patterns:
- No analytics; tags accumulate without curation
- Auto-tag suggestions that overrule user choice
Output:
1. Metrics tracking
2. Tag-cloud visualization
3. Duplicate detection
4. Related-tag suggestion
5. Admin dashboard
The "unused tags accumulate" reality: 80% of tags get used 1-2 times. Periodic cleanup (delete stale; merge dupes) keeps tag system manageable.
8. AI-suggested tags
LLMs can auto-tag content based on text.
Implement AI tag suggestions.
Pattern:
On create / update of resource:
- Send content to LLM (Claude / GPT-4o)
- Prompt: "Suggest tags from this list: [tags]. Or suggest new ones. Content: [...]"
- Return: array of tag names + confidence
Display:
- Suggested tags appear as ghost-pills (not yet applied)
- User clicks to confirm / dismiss
- Or: auto-apply if confidence > 0.9
Best for:
- Email categorization
- Support ticket triage
- Document content classification
- Content management
Cost considerations:
- Per-resource LLM call
- Cache by content hash
- Batch for bulk operations
Quality:
- LLM tags often overlap with existing
- May suggest creating new ones (allow toggle)
- Refine prompt for your domain
User control:
- Always allow override
- Confidence threshold
- Disable per-user / per-workspace
Privacy:
- Don't send sensitive content to public LLM without checks
- On-prem / private LLMs for high-security
Output:
1. AI suggestion flow
2. Display pattern (ghost vs auto)
3. Cost optimization
4. Privacy considerations
5. User control settings
The "auto-tag with high confidence" pattern: works well for repetitive content (email triage). For knowledge work (notes), suggest only; let user choose.
9. Cross-resource tag operations
Tags connect resources. Make connections useful.
Cross-resource tag operations.
Pattern: click tag → see all resources with that tag
UI:
- Tag pill is clickable
- Clicks navigate to filtered view
- Or: opens panel with related resources
Tag pages (Notion-style):
- Each tag has its own page
- Lists resources with that tag
- Description / notes on the tag itself
- Shareable URL
Tag-based dashboards:
- "Show me all tasks tagged 'urgent'"
- Cross-project / cross-team aggregation
Bulk operations on tagged set:
- Select all "bug"-tagged tasks → bulk action
- Apply / remove tag on bulk
Export:
- Export resources matching tag(s)
- CSV / JSON
Search integration:
- "tag:bug urgent" → search content + tags
- Tag becomes search dimension
Output:
1. Tag-page implementation
2. Cross-resource filtering
3. Bulk operations
4. Search integration
5. Export
The Notion tag-page pattern: each tag is its own discoverable entity. Click tag → see everything with it. Powerful for organization.
10. Permissions + governance
In multi-user products, tag governance matters.
Tag permissions and governance.
Who can:
Apply tags:
- All members (default; flexible)
- Or: only specific roles
- Configurable per workspace
Create new tags:
- All members (most products)
- Or: only admins (controlled)
- Avoids tag explosion
Edit / rename tags:
- Tag creator + admins
- Affects everyone (cascade)
Delete tags:
- Tag creator + admins
- Confirm cascade (resources untagged)
Color / metadata:
- Same as edit permissions
Audit log:
- Tag created / modified / deleted
- Who, when, what changed
Tag locks:
- "System tags" cannot be deleted (e.g., "high-priority" used in automations)
- "Approved tags" curated by admins
Domain governance:
- Tag taxonomy guides (admins set guidelines)
- Naming conventions (e.g., kebab-case)
- Periodic cleanup events
Anti-patterns:
- Anyone can delete any tag (chaos)
- No audit log (untraceable changes)
- Strict admin-only creation (slow; users want flexibility)
Output:
1. Permission matrix
2. Audit logging
3. System / locked tags
4. Governance documentation
5. Cleanup cadence
The "open create + admin curate" balance: members can create tags freely (low friction), admins periodically review + merge (governance). Otherwise tags either explode or stagnate.
What Done Looks Like
A v1 tags / labels system for B2B SaaS in 2026:
- Schema with workspace-scoped tags + many-to-many resource_tags
- Tag combobox UI with create-on-the-fly
- Color-coded tags (auto-hashed or user-chosen)
- Tag management page (CRUD + merge)
- Tag-based filtering (single + multi)
- Click-to-filter from tag pill
- URL state for filters
- Multi-tenant isolation
- Cascade rules on tag delete
- Permissions (apply / create / delete)
- Mobile-friendly UX
Add later when product is mature:
- AI tag suggestions
- Tag pages (Notion-style)
- Tag analytics dashboard
- Bulk tag operations
- Tag templates
- Tag governance + audit
The mistake to avoid: comma-separated string column for tags. Querying / filtering / merging / counting becomes pain.
The second mistake: no merge feature. Duplicates accumulate; tag chaos.
The third mistake: tag explosion without governance. 1000+ tags = effectively no tags. Periodic cleanup needed.
See Also
- Data Tables: Sort, Filter, Pagination, Bulk Actions — table filter integration
- Search & Autocomplete Typeahead — adjacent search UX
- Comments, Threading & @Mentions — adjacent rich UX
- Multi-Tenancy — workspace scoping
- Roles & Permissions — permission model
- Inline Editing Patterns — inline tag input
- Drag-and-Drop & Kanban Boards — kanban with tag swimlanes
- AI Features Implementation — AI tagging
- Empty States, Loading & Error States — empty filter states
- Audit Logs — tag change audit
- Workspace, Org & Tenant Switcher — workspace scoping
- VibeReference: Components — UI primitives
- VibeReference: shadcn/ui — Combobox + Badge
- VibeReference: Anthropic Claude — AI tag suggestions
- LaunchWeek: Documentation Strategy — tags in docs