Brik Design System
Components

Catalog picker

Industry-aware multi-pick with source attribution. Catalog picks + free-text additions, all stably slugged.

CatalogPicker is the right primitive when an industry pack seeds defaults that the client can extend. It renders a reference catalog (e.g. the BCS dental industry pack's servicesCatalog) as suggestions, accepts free-text additions, and stamps every entry with a stable slug + source: 'catalog' | 'custom' attribution.

Use it for

  • Any client-intel field where the industry pack seeds defaults the client can extend (services offered, pain points, competitor archetypes, keyword banks)
  • Fields where provenance matters — the difference between "this came from our industry default" and "this is client-specific" changes downstream behavior (copy generation, mockup content, competitive analysis framing)
  • Multi-pick controls where each entry needs a description alongside the name

For locked vocabularies (no custom escape), use MultiSelect. For flat string picks without descriptions, use AddableComboList. For structured entries with fields other than description (URL + notes, name + role), use AddableEntryList directly.

Import

import { CatalogPicker } from '@brikdesigns/bds';
import { getIndustryServicesCatalog } from '@brikdesigns/bds/content-system';

Variants

Default

const catalog = getIndustryServicesCatalog(company.industry_slug);

<CatalogPicker
  label="Services Offered"
  catalog={catalog}
  value={services}
  onChange={setServices}
  searchPlaceholder="Search or add a service…"
  descriptionPlaceholder="What makes this service unique here?"
  addLabel="Add Service"
  emptyDescriptionLabel="No description set"
/>

Strict (catalog-only)

strict rejects free-text names that don't match a catalog displayName or alias. Use for locked vocabularies that still benefit from the catalog's metadata + description-per-entry shape.

<CatalogPicker
  label="Insurance accepted"
  catalog={insuranceCatalog}
  value={insurances}
  onChange={setInsurances}
  strict
/>

Different industry, same component

The picker is vocabulary-agnostic — pass any catalog matching the CatalogEntry shape. Real estate, dental, commercial — same picker, different catalog prop.

<CatalogPicker
  label="Property types"
  catalog={getIndustryPropertyTypesCatalog('real-estate-rv-mhc')}
  value={propertyTypes}
  onChange={setPropertyTypes}
/>

Sizes

sm, md (default), lg — matching the AddableEntryList scale.

Disabled

<CatalogPicker disabled value={services} onChange={() => {}} catalog={catalog} />

Source attribution

Every picked entry carries source: 'catalog' | 'custom':

  • catalog — matched the catalog by exact displayName or alias (case-insensitive). The picker assigns the catalog entry's canonical slug.
  • custom — no catalog match. The picker derives a kebab-case slug from the typed displayName. If that slug collides with an existing one, -2, -3, … append until unique.

Consumers persist the full array as a single JSONB column (e.g. company_profiles.services_offered). Downstream content generation reads the structure without re-deriving provenance every time.

// What the picker emits via onChange
[
  { source: 'catalog', slug: 'cosmetic-veneers', displayName: 'Veneers', description: 'Composite + porcelain' },
  { source: 'custom', slug: 'in-house-membership', displayName: 'In-house membership plan', description: 'No PPO routing' },
]

When not to use

Don't use CatalogPicker for locked vocabularies with no custom escape. That's MultiSelect's job — pass the catalog as options. CatalogPicker's value is the escape hatch for free-text additions; if you don't want one, use the simpler component.

  • Don't use for flat string picks with no description. The description textarea adds vertical weight that's wasted on tag-only inputs — use AddableComboList.
  • Don't use for structured entries with fields other than description (URL + notes, name + role). Use AddableEntryList directly; you don't need catalog wiring.

Accessibility

  • The search input is a real <input> — keyboard, autocomplete, screen reader behaviors all platform.
  • Each picked entry has a labeled remove button (removeLabel prop, defaults to Remove {name}).
  • Description textareas autosize and announce row counts.

API

PropTypeDefault
catalogreadonly CatalogEntry[] (required)
valuereadonly PickedCatalogEntry[] (required)
onChange(next: PickedCatalogEntry[]) => void (required)
labelstring
helperTextstring
searchPlaceholderstring
descriptionPlaceholderstring
addLabelstring'Add'
removeLabelstringauto
emptyLabelstring
emptyDescriptionLabelstring'No description'
size'sm' | 'md' | 'lg''md'
disabledbooleanfalse
strictbooleanfalse
maxItemsnumber
descriptionRowsnumber2
classNamestring

CatalogEntry / PickedCatalogEntry shapes

interface CatalogEntry {
  slug: string;
  displayName: string;
  aliases?: string[];
  description?: string;
}

interface PickedCatalogEntry {
  source: 'catalog' | 'custom';
  slug: string;
  displayName: string;
  description?: string;
}

On this page