ACF UI
Components

Relationship

A dual-column component for selecting one or more posts, pages, or custom content items with search and filtering capabilities.

Related Posts

Select up to 3 related posts to display

Choose posts that are related to the current content

Getting Started with React

post

Advanced TypeScript Patterns

post

Building UIs with Tailwind CSS

postdraft

State Management Guide

guide

API Design Best Practices

guide

Component Architecture

tutorial
No items selected
0 selected / 3 max
0 selected / 3 max
"use client"
 
import { useState } from "react"
import { Relationship, type RelationshipItem } from "@/components/ui/relationship"
 
const posts = [
  {
    id: 1,
    title: "Getting Started with React",
    type: "post",
    status: "publish" as const,
  },
  {
    id: 2,
    title: "Advanced TypeScript Patterns",
    type: "post", 
    status: "publish" as const,
  },
  {
    id: 3,
    title: "Building UIs with Tailwind CSS",
    type: "post",
    status: "draft" as const,
  },
  {
    id: 4,
    title: "State Management Guide",
    type: "guide",
    status: "publish" as const,
  },
  {
    id: 5,
    title: "API Design Best Practices",
    type: "guide",
    status: "publish" as const,
  },
  {
    id: 6,
    title: "Component Architecture",
    type: "tutorial",
    status: "publish" as const,
  },
] satisfies RelationshipItem[]
 
export function RelationshipDemo() {
  const [selectedPosts, setSelectedPosts] = useState<RelationshipItem[]>([])
 
  return (
    <div className="w-full max-w-4xl">
      <div className="space-y-4">
        <div>
          <h3 className="text-lg font-semibold">Related Posts</h3>
          <p className="text-sm text-muted-foreground">
            Select up to 3 related posts to display
          </p>
        </div>
        
        <p className="text-sm text-muted-foreground">
          Choose posts that are related to the current content
        </p>
 
        <Relationship
          items={posts}
          selectedItems={selectedPosts}
          onChange={(items) => setSelectedPosts(items as RelationshipItem[])}
          multiple={true}
          max={3}
          showFilters={["search", "post_type", "taxonomy"]}
          postTypes={["post", "guide", "tutorial"]}
          taxonomies={["React", "JavaScript", "TypeScript", "CSS", "Tailwind"]}
          searchPlaceholder="Search posts..."
        />
        
        <div className="text-sm text-muted-foreground">
          {selectedPosts.length} selected / 3 max
        </div>
      </div>
    </div>
  )
} 

Installation

CLI

npx shadcn@latest add "https://acfui.com/r/relationship"
pnpm dlx shadcn@latest add "https://acfui.com/r/relationship"
yarn dlx shadcn@latest add "https://acfui.com/r/relationship"
bun x shadcn@latest add "https://acfui.com/r/relationship"

Manual

Install the following dependencies:

npm install lucide-react class-variance-authority
pnpm add lucide-react class-variance-authority
yarn add lucide-react class-variance-authority
bun add lucide-react class-variance-authority

Copy and paste the following code into your project.

"use client"
 
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import {
  Calendar,
  Eye,
  FileText,
  GripVertical,
  Plus,
  Search,
  X,
} from "lucide-react"
 
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
import {
  Card,
  CardContent,
  CardHeader,
  CardTitle,
} from "@/components/ui/card"
 
const relationshipVariants = cva("border rounded-lg bg-background", {
  variants: {
    variant: {
      default: "border-border",
      error: "border-destructive",
    },
    size: {
      sm: "text-sm",
      md: "text-base",
      lg: "text-lg",
    },
  },
  defaultVariants: {
    variant: "default",
    size: "md",
  },
})
 
export interface RelationshipItem {
  id: string | number
  title: string
  type?: string
  status?: "publish" | "draft" | "private"
  taxonomy?: string[]
  featured_image?: string
  excerpt?: string
  date?: string
  url?: string
}
 
export interface RelationshipFilter {
  search?: string
  post_type?: string[]
  post_status?: string[]
  taxonomy?: string[]
}
 
export interface RelationshipProps
  extends Omit<
      React.HTMLAttributes<HTMLDivElement>,
      "onChange" | "onLoad" | "onSelect" | "onRemove"
    >,
    VariantProps<typeof relationshipVariants> {
  // Data props
  items?: RelationshipItem[]
  selectedItems?: RelationshipItem[]
 
  // Configuration
  multiple?: boolean
  min?: number
  max?: number
  required?: boolean
 
  // Async data loading
  onLoad?: (filter: RelationshipFilter) => Promise<RelationshipItem[]>
  loading?: boolean
 
  // Filtering options
  postTypes?: string[]
  postStatuses?: string[]
  taxonomies?: string[]
  showFilters?: boolean | string[] // true/false or ['search', 'post_type', 'taxonomy']
 
  // Display options
  showPreview?: boolean
  showFeaturedImage?: boolean
  showExcerpt?: boolean
  showDate?: boolean
  sortable?: boolean
 
  // Return format
  returnFormat?: "object" | "id"
 
  // Event handlers
  onChange?: (items: RelationshipItem[] | string[] | number[]) => void
  onSelect?: (item: RelationshipItem) => void
  onRemove?: (item: RelationshipItem) => void
 
  // Labels and messages
  searchPlaceholder?: string
  noItemsText?: string
  noSelectedText?: string
  loadingText?: string
  instructions?: string
 
  // Validation
  error?: string
}
 
const Relationship = React.forwardRef<HTMLDivElement, RelationshipProps>(
  (
    {
      className,
      variant,
      size,
      items = [],
      selectedItems = [],
      multiple = true,
      min = 0,
      max = 10,
      required = false,
      onLoad,
      loading = false,
      postTypes = [],
      postStatuses = [],
      taxonomies = [],
      showFilters = true,
      showPreview = false,
      showFeaturedImage = false,
      showExcerpt = false,
      showDate = false,
      sortable = false,
      returnFormat = "object",
      onChange,
      onSelect,
      onRemove,
      searchPlaceholder = "Search...",
      noItemsText = "No items found",
      noSelectedText = "No items selected",
      loadingText = "Loading...",
      instructions,
      error,
      ...props
    },
    ref
  ) => {
    const [searchQuery, setSearchQuery] = React.useState("")
    const [selectedPostType, setSelectedPostType] = React.useState<string>("all")
    const [selectedPostStatus, setSelectedPostStatus] = React.useState<string>("all")
    const [selectedTaxonomy, setSelectedTaxonomy] = React.useState<string>("all")
    const [availableItems, setAvailableItems] = React.useState<RelationshipItem[]>(items)
    const [internalSelected, setInternalSelected] = React.useState<RelationshipItem[]>(selectedItems)
    const [isLoading, setIsLoading] = React.useState(loading)
 
    // Debounced search
    React.useEffect(() => {
      const timeoutId = setTimeout(() => {
        if (onLoad) {
          setIsLoading(true)
          const filter: RelationshipFilter = {
            search: searchQuery || undefined,
            post_type: selectedPostType === "all" ? postTypes : [selectedPostType],
            post_status: selectedPostStatus === "all" ? postStatuses : [selectedPostStatus],
            taxonomy: selectedTaxonomy === "all" ? undefined : [selectedTaxonomy],
          }
 
          onLoad(filter)
            .then((loadedItems) => {
              setAvailableItems(loadedItems || [])
              setIsLoading(false)
            })
            .catch(() => {
              setAvailableItems([])
              setIsLoading(false)
            })
        }
      }, 300)
 
      return () => clearTimeout(timeoutId)
    }, [searchQuery, selectedPostType, selectedPostStatus, selectedTaxonomy, onLoad, postTypes, postStatuses])
 
    // Update internal state when selectedItems prop changes
    React.useEffect(() => {
      setInternalSelected(selectedItems || [])
    }, [selectedItems])
 
    // Update available items when items prop changes
    React.useEffect(() => {
      if (!onLoad) {
        setAvailableItems(items || [])
      }
    }, [items, onLoad])
 
    const handleSelectItem = React.useCallback(
      (item: RelationshipItem) => {
        if (!multiple && internalSelected.length >= 1) return
        if (internalSelected.some((selected) => selected.id === item.id)) return
        if (internalSelected.length >= max) return
 
        const newSelected = multiple ? [...internalSelected, item] : [item]
        setInternalSelected(newSelected)
 
        onSelect?.(item)
 
        if (onChange) {
          const result = returnFormat === "object" 
            ? newSelected 
            : newSelected.map((item) => item.id) as string[] | number[]
          onChange(result)
        }
      },
      [internalSelected, multiple, max, returnFormat, onChange, onSelect]
    )
 
    const handleRemoveItem = React.useCallback(
      (item: RelationshipItem) => {
        const newSelected = internalSelected.filter(
          (selected) => selected.id !== item.id
        )
        setInternalSelected(newSelected)
 
        onRemove?.(item)
 
        if (onChange) {
          const result = returnFormat === "object" 
            ? newSelected 
            : newSelected.map((item) => item.id) as string[] | number[]
          onChange(result)
        }
      },
      [internalSelected, returnFormat, onChange, onRemove]
    )
 
    const getFilteredItems = React.useCallback(() => {
      if (onLoad) return availableItems.filter(item => item && item.id !== undefined)
 
      return availableItems.filter((item) => {
        if (!item || item.id === undefined) return false
        
        const matchesSearch = !searchQuery || 
          item.title?.toLowerCase().includes(searchQuery.toLowerCase())
 
        const matchesPostType = selectedPostType === "all" || 
          !selectedPostType || item.type === selectedPostType
 
        const matchesPostStatus = selectedPostStatus === "all" || 
          !selectedPostStatus || item.status === selectedPostStatus
 
        const matchesTaxonomy = selectedTaxonomy === "all" || 
          !selectedTaxonomy || item.taxonomy?.includes(selectedTaxonomy)
 
        return matchesSearch && matchesPostType && matchesPostStatus && matchesTaxonomy
      })
    }, [availableItems, searchQuery, selectedPostType, selectedPostStatus, selectedTaxonomy, onLoad])
 
    const filteredItems = getFilteredItems()
    const isFiltersVisible = showFilters === true || (Array.isArray(showFilters) && showFilters.length > 0)
 
    const shouldShowSearch = showFilters === true || (Array.isArray(showFilters) && showFilters.includes("search"))
    const shouldShowPostType = showFilters === true || (Array.isArray(showFilters) && showFilters.includes("post_type"))
    const shouldShowPostStatus = showFilters === true || (Array.isArray(showFilters) && showFilters.includes("post_status"))
    const shouldShowTaxonomy = showFilters === true || (Array.isArray(showFilters) && showFilters.includes("taxonomy"))
 
    return (
      <div
        ref={ref}
        className={cn(relationshipVariants({ variant, size, className }))}
        {...props}
      >
        {/* Instructions */}
        {instructions && (
          <div className="border-b p-4">
            <p className="text-sm text-muted-foreground">{instructions}</p>
          </div>
        )}
 
        {/* Filters */}
        {isFiltersVisible && (
          <div className="space-y-3 border-b p-4">
            <div className="flex flex-col gap-3 sm:flex-row">
              {shouldShowSearch && (
                <div className="relative flex-1">
                  <Search className="text-muted-foreground absolute top-1/2 left-3 size-4 -translate-y-1/2 transform" />
                  <Input
                    placeholder={searchPlaceholder}
                    value={searchQuery}
                    onChange={(e) => setSearchQuery(e.target.value)}
                    className="pl-9"
                  />
                </div>
              )}
 
              {shouldShowPostType && postTypes.length > 0 && (
                <Select value={selectedPostType} onValueChange={setSelectedPostType}>
                  <SelectTrigger className="w-full sm:w-[180px]">
                    <SelectValue placeholder="Post type" />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="all">All types</SelectItem>
                    {postTypes.map((type) => (
                      <SelectItem key={type} value={type}>
                        {type}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              )}
 
              {shouldShowPostStatus && postStatuses.length > 0 && (
                <Select value={selectedPostStatus} onValueChange={setSelectedPostStatus}>
                  <SelectTrigger className="w-full sm:w-[180px]">
                    <SelectValue placeholder="Status" />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="all">All statuses</SelectItem>
                    {postStatuses.map((status) => (
                      <SelectItem key={status} value={status}>
                        {status}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              )}
 
              {shouldShowTaxonomy && taxonomies.length > 0 && (
                <Select value={selectedTaxonomy} onValueChange={setSelectedTaxonomy}>
                  <SelectTrigger className="w-full sm:w-[180px]">
                    <SelectValue placeholder="Category" />
                  </SelectTrigger>
                  <SelectContent>
                    <SelectItem value="all">All categories</SelectItem>
                    {taxonomies.map((taxonomy) => (
                      <SelectItem key={taxonomy} value={taxonomy}>
                        {taxonomy}
                      </SelectItem>
                    ))}
                  </SelectContent>
                </Select>
              )}
            </div>
          </div>
        )}
 
        {/* Main Content */}
        <div className="grid grid-cols-1 lg:grid-cols-2">
          {/* Available Items */}
          <Card className="flex flex-col h-96 border-none rounded-none p-0">
            <CardContent className="flex-1 overflow-hidden p-0">
              <div className="h-full overflow-y-auto px-4 pt-4 pb-4">
                {loading ? (
                  <div className="flex items-center justify-center h-32 text-sm text-muted-foreground">
                    {loadingText}
                  </div>
                ) : filteredItems.length === 0 ? (
                  <div className="flex items-center justify-center h-32 text-sm text-muted-foreground">
                    {noItemsText}
                  </div>
                ) : (
                  <div className="space-y-2">
                    {filteredItems.map((item) => {
                      const isSelected = internalSelected.some((selected) => selected.id === item.id)
                      
                      return (
                        <div
                          key={item.id}
                          className={cn(
                            "group border rounded-lg p-3 transition-colors",
                            isSelected
                              ? "bg-muted/50 border-muted cursor-default opacity-60"
                              : "cursor-pointer hover:bg-accent"
                          )}
                          onClick={!isSelected ? () => handleSelectItem(item) : undefined}
                        >
                          <div className="flex items-center gap-3">
                            {showFeaturedImage && item.featured_image && (
                              <img
                                src={item.featured_image}
                                alt={item.title}
                                className="w-8 h-8 rounded object-cover flex-shrink-0"
                              />
                            )}
                            <div className="flex-1 min-w-0">
                              <h4 className="font-medium text-sm leading-tight truncate">
                                {item.title}
                              </h4>
                              {showExcerpt && item.excerpt && (
                                <p className="text-xs text-muted-foreground mt-1 line-clamp-2">
                                  {item.excerpt}
                                </p>
                              )}
                              <div className="flex items-center gap-2 mt-1">
                                {item.type && (
                                  <Badge variant="secondary" className="text-xs h-5">
                                    {item.type}
                                  </Badge>
                                )}
                                {item.status && item.status !== "publish" && (
                                  <Badge variant="outline" className="text-xs h-5">
                                    {item.status}
                                  </Badge>
                                )}
                                {item.taxonomy?.map((tax) => (
                                  <Badge key={tax} variant="outline" className="text-xs h-5">
                                    {tax}
                                  </Badge>
                                ))}
                                {showDate && item.date && (
                                  <span className="text-xs text-muted-foreground">
                                    {item.date}
                                  </span>
                                )}
                              </div>
                            </div>
                            <Button
                              size="sm"
                              variant={isSelected ? "secondary" : "outline"}
                              className="h-8 w-8 p-0 flex-shrink-0"
                              disabled={isSelected}
                            >
                              {isSelected ? (
                                <X className="h-3 w-3" />
                              ) : (
                                <Plus className="h-3 w-3" />
                              )}
                            </Button>
                          </div>
                          {showPreview && item.url && (
                            <div className="mt-2 pt-2 border-t">
                              <Button
                                variant="link"
                                size="sm"
                                className="h-auto p-0 text-xs"
                                asChild
                              >
                                <a href={item.url} target="_blank" rel="noopener noreferrer">
                                  <Eye className="h-3 w-3 mr-1" />
                                  Preview
                                </a>
                              </Button>
                            </div>
                          )}
                        </div>
                      )
                    })}
                  </div>
                )}
              </div>
            </CardContent>
          </Card>
 
          {/* Selected Items */}
          <Card className="flex flex-col h-96 border-none rounded-none p-0">
            <CardContent className="flex-1 overflow-hidden p-0">
              <div className="h-full overflow-y-auto px-4 pt-4 pb-4">
                {internalSelected.length === 0 ? (
                  <div className="flex items-center justify-center h-32 text-sm text-muted-foreground">
                    {noSelectedText}
                  </div>
                ) : (
                  <div className="space-y-2">
                    {internalSelected.map((item) => (
                      <div
                        key={item.id}
                        className="group border rounded-lg p-3 bg-accent/50"
                      >
                        <div className="flex items-center gap-3">
                          {sortable && (
                            <Button
                              size="sm"
                              variant="ghost"
                              className="h-8 w-8 p-0 cursor-grab flex-shrink-0"
                            >
                              <GripVertical className="h-3 w-3" />
                            </Button>
                          )}
                          {showFeaturedImage && item.featured_image && (
                            <img
                              src={item.featured_image}
                              alt={item.title}
                              className="w-8 h-8 rounded object-cover flex-shrink-0"
                            />
                          )}
                          <div className="flex-1 min-w-0">
                            <h4 className="font-medium text-sm leading-tight truncate">
                              {item.title}
                            </h4>
                            {showExcerpt && item.excerpt && (
                              <p className="text-xs text-muted-foreground mt-1 line-clamp-2">
                                {item.excerpt}
                              </p>
                            )}
                            <div className="flex items-center gap-2 mt-1">
                              {item.type && (
                                <Badge variant="secondary" className="text-xs h-5">
                                  {item.type}
                                </Badge>
                              )}
                              {item.status && item.status !== "publish" && (
                                <Badge variant="outline" className="text-xs h-5">
                                  {item.status}
                                </Badge>
                              )}
                              {item.taxonomy?.map((tax) => (
                                <Badge key={tax} variant="outline" className="text-xs h-5">
                                  {tax}
                                </Badge>
                              ))}
                              {showDate && item.date && (
                                <span className="text-xs text-muted-foreground">
                                  {item.date}
                                </span>
                              )}
                            </div>
                          </div>
                          <Button
                            size="sm"
                            variant="ghost"
                            className="h-8 w-8 p-0 flex-shrink-0"
                            onClick={() => handleRemoveItem(item)}
                          >
                            <X className="h-3 w-3" />
                          </Button>
                        </div>
                        {showPreview && item.url && (
                          <div className="mt-2 pt-2 border-t">
                            <Button
                              variant="link"
                              size="sm"
                              className="h-auto p-0 text-xs"
                              asChild
                            >
                              <a href={item.url} target="_blank" rel="noopener noreferrer">
                                <Eye className="h-3 w-3 mr-1" />
                                Preview
                              </a>
                            </Button>
                          </div>
                        )}
                      </div>
                    ))}
                  </div>
                )}
              </div>
            </CardContent>
          </Card>
        </div>
 
        {/* Footer */}
        <div className="bg-muted/30 text-muted-foreground border-t p-3 text-xs">
          <div className="flex items-center justify-between">
            <div>
              {required && internalSelected.length < min && (
                <span className="text-destructive">
                  Minimum {min} item{min !== 1 ? "s" : ""} required
                </span>
              )}
              {min > 0 && internalSelected.length >= min && (
                <span className="text-green-600">
                  {internalSelected.length} selected / {min} min required
                </span>
              )}
            </div>
            <div>
              {internalSelected.length} selected{max < Infinity && ` / ${max} max`}
            </div>
          </div>
        </div>
 
        {/* Error Message */}
        {error && (
          <div className="border-destructive bg-destructive/10 border-t p-3">
            <p className="text-destructive text-sm">{error}</p>
          </div>
        )}
      </div>
    )
  }
)
Relationship.displayName = "Relationship"
 
export { Relationship, relationshipVariants }

Update the import paths to match your project setup.

Usage

import { Relationship } from "@/components/ui/relationship"
<Relationship
  items={posts}
  selectedItems={selectedPosts}
  onChange={(items) => setSelectedPosts(items)}
  multiple={true}
  max={5}
/>

Examples

Basic

A basic relationship field with static data, search, and filtering capabilities.

Related Posts

Select up to 3 related posts to display

Choose posts that are related to the current content

Getting Started with React

post

Advanced TypeScript Patterns

post

Building UIs with Tailwind CSS

postdraft

State Management Guide

guide

API Design Best Practices

guide

Component Architecture

tutorial
No items selected
0 selected / 3 max
0 selected / 3 max
"use client"
 
import { useState } from "react"
import { Relationship, type RelationshipItem } from "@/components/ui/relationship"
 
const posts = [
  {
    id: 1,
    title: "Getting Started with React",
    type: "post",
    status: "publish" as const,
  },
  {
    id: 2,
    title: "Advanced TypeScript Patterns",
    type: "post", 
    status: "publish" as const,
  },
  {
    id: 3,
    title: "Building UIs with Tailwind CSS",
    type: "post",
    status: "draft" as const,
  },
  {
    id: 4,
    title: "State Management Guide",
    type: "guide",
    status: "publish" as const,
  },
  {
    id: 5,
    title: "API Design Best Practices",
    type: "guide",
    status: "publish" as const,
  },
  {
    id: 6,
    title: "Component Architecture",
    type: "tutorial",
    status: "publish" as const,
  },
] satisfies RelationshipItem[]
 
export function RelationshipDemo() {
  const [selectedPosts, setSelectedPosts] = useState<RelationshipItem[]>([])
 
  return (
    <div className="w-full max-w-4xl">
      <div className="space-y-4">
        <div>
          <h3 className="text-lg font-semibold">Related Posts</h3>
          <p className="text-sm text-muted-foreground">
            Select up to 3 related posts to display
          </p>
        </div>
        
        <p className="text-sm text-muted-foreground">
          Choose posts that are related to the current content
        </p>
 
        <Relationship
          items={posts}
          selectedItems={selectedPosts}
          onChange={(items) => setSelectedPosts(items as RelationshipItem[])}
          multiple={true}
          max={3}
          showFilters={["search", "post_type", "taxonomy"]}
          postTypes={["post", "guide", "tutorial"]}
          taxonomies={["React", "JavaScript", "TypeScript", "CSS", "Tailwind"]}
          searchPlaceholder="Search posts..."
        />
        
        <div className="text-sm text-muted-foreground">
          {selectedPosts.length} selected / 3 max
        </div>
      </div>
    </div>
  )
} 

Async Loading

Load data asynchronously with search and filtering. Perfect for large datasets that need server-side filtering.

Async Content Loader

Search and filter from a large dataset with async loading

Start typing to search through our content database

No items found
No items selected
0 selected / 5 max
"use client"
 
import { useState } from "react"
import { Relationship, type RelationshipItem, type RelationshipFilter } from "@/components/ui/relationship"
 
// Simulate a large dataset
const mockDatabase: RelationshipItem[] = [
  {
    id: 1,
    title: "Introduction to React Hooks",
    type: "post",
    status: "publish",
    taxonomy: ["React", "JavaScript", "Frontend"],
    excerpt: "Learn how to use React Hooks to manage state and side effects in functional components...",
    date: "2024-01-15",
    featured_image: "https://picsum.photos/400/300?random=1",
    url: "/posts/react-hooks-intro",
  },
  {
    id: 2,
    title: "Advanced TypeScript Patterns",
    type: "post",
    status: "publish",
    taxonomy: ["TypeScript", "Advanced", "Development"],
    excerpt: "Explore advanced TypeScript patterns including conditional types, mapped types, and template literals...",
    date: "2024-01-10",
    featured_image: "https://picsum.photos/400/300?random=2",
    url: "/posts/advanced-typescript",
  },
  {
    id: 3,
    title: "Building Responsive UIs",
    type: "tutorial",
    status: "publish",
    taxonomy: ["CSS", "Responsive", "UI"],
    excerpt: "Create beautiful, responsive user interfaces that work on all devices...",
    date: "2024-01-05",
    featured_image: "https://picsum.photos/400/300?random=3",
    url: "/tutorials/responsive-ui",
  },
  {
    id: 4,
    title: "State Management with Zustand",
    type: "guide",
    status: "publish",
    taxonomy: ["State Management", "Zustand", "React"],
    excerpt: "Learn how to manage application state efficiently with Zustand...",
    date: "2024-01-20",
    featured_image: "https://picsum.photos/400/300?random=4",
    url: "/guides/zustand-state",
  },
  {
    id: 5,
    title: "API Design Best Practices",
    type: "guide",
    status: "draft",
    taxonomy: ["API", "Backend", "REST"],
    excerpt: "Design robust and maintainable APIs that scale with your application...",
    date: "2024-01-12",
    featured_image: "https://picsum.photos/400/300?random=5",
    url: "/guides/api-design",
  },
  {
    id: 6,
    title: "Component Architecture Patterns",
    type: "tutorial",
    status: "publish",
    taxonomy: ["Architecture", "Components", "React"],
    excerpt: "Build scalable component architectures with these proven patterns...",
    date: "2024-01-08",
    featured_image: "https://picsum.photos/400/300?random=6",
    url: "/tutorials/component-architecture",
  },
  {
    id: 7,
    title: "Testing React Components",
    type: "post",
    status: "publish",
    taxonomy: ["Testing", "React", "Jest"],
    excerpt: "Write comprehensive tests for your React components using Jest and React Testing Library...",
    date: "2024-01-25",
    featured_image: "https://picsum.photos/400/300?random=7",
    url: "/posts/testing-react",
  },
  {
    id: 8,
    title: "Performance Optimization",
    type: "guide",
    status: "publish",
    taxonomy: ["Performance", "Optimization", "React"],
    excerpt: "Optimize your React applications for better performance and user experience...",
    date: "2024-01-18",
    featured_image: "https://picsum.photos/400/300?random=8",
    url: "/guides/performance-optimization",
  },
  {
    id: 9,
    title: "Server-Side Rendering with Next.js",
    type: "tutorial",
    status: "draft",
    taxonomy: ["Next.js", "SSR", "React"],
    excerpt: "Implement server-side rendering for better SEO and performance...",
    date: "2024-01-03",
    featured_image: "https://picsum.photos/400/300?random=9",
    url: "/tutorials/nextjs-ssr",
  },
  {
    id: 10,
    title: "Database Design Fundamentals",
    type: "guide",
    status: "publish",
    taxonomy: ["Database", "SQL", "Backend"],
    excerpt: "Learn the fundamentals of database design and normalization...",
    date: "2024-01-14",
    featured_image: "https://picsum.photos/400/300?random=10",
    url: "/guides/database-design",
  },
]
 
// Simulate API call with delay
const fetchPosts = async (filter: RelationshipFilter): Promise<RelationshipItem[]> => {
  // Simulate network delay
  await new Promise(resolve => setTimeout(resolve, 800))
  
  let filteredPosts = mockDatabase
  
  // Apply search filter
  if (filter.search) {
    const searchTerm = filter.search.toLowerCase()
    filteredPosts = filteredPosts.filter(post => 
      post.title.toLowerCase().includes(searchTerm) ||
      post.excerpt?.toLowerCase().includes(searchTerm) ||
      post.taxonomy?.some(tag => tag.toLowerCase().includes(searchTerm))
    )
  }
  
  // Apply post type filter
  if (filter.post_type && filter.post_type.length > 0) {
    filteredPosts = filteredPosts.filter(post => 
      filter.post_type!.includes(post.type || "")
    )
  }
  
  // Apply post status filter
  if (filter.post_status && filter.post_status.length > 0) {
    filteredPosts = filteredPosts.filter(post => 
      filter.post_status!.includes(post.status || "")
    )
  }
  
  // Apply taxonomy filter
  if (filter.taxonomy && filter.taxonomy.length > 0) {
    filteredPosts = filteredPosts.filter(post => 
      post.taxonomy?.some(tag => filter.taxonomy!.includes(tag))
    )
  }
  
  return filteredPosts
}
 
export function RelationshipAsyncDemo() {
  const [selectedPosts, setSelectedPosts] = useState<RelationshipItem[]>([])
 
  return (
    <div className="w-full max-w-4xl mx-auto space-y-4">
      <div>
        <h3 className="text-lg font-semibold mb-2">Async Content Loader</h3>
        <p className="text-sm text-muted-foreground mb-4">
          Search and filter from a large dataset with async loading
        </p>
      </div>
 
      <Relationship
        onLoad={fetchPosts}
        selectedItems={selectedPosts}
        onChange={(items) => setSelectedPosts(items as RelationshipItem[])}
        multiple={true}
        max={5}
        postTypes={["post", "guide", "tutorial"]}
        postStatuses={["publish", "draft"]}
        taxonomies={["React", "TypeScript", "CSS", "API", "Testing", "Performance"]}
        showFeaturedImage={true}
        showExcerpt={true}
        showDate={true}
        showPreview={true}
        instructions="Start typing to search through our content database"
        searchPlaceholder="Search posts by title, content, or tags..."
        loadingText="Searching content..."
      />
 
      {selectedPosts.length > 0 && (
        <div className="mt-8 p-4 bg-muted/30 rounded-lg">
          <h4 className="font-medium mb-2">Selected Content ({selectedPosts.length}):</h4>
          <div className="grid gap-2">
            {selectedPosts.map((post) => (
              <div key={post.id} className="flex items-center gap-3 p-2 bg-background rounded border">
                {post.featured_image && (
                  <img
                    src={post.featured_image}
                    alt={post.title}
                    className="w-12 h-12 rounded object-cover"
                  />
                )}
                <div className="flex-1">
                  <div className="font-medium text-sm">{post.title}</div>
                  <div className="text-xs text-muted-foreground">
                    {post.type} • {post.date}
                  </div>
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  )
} 

Single Selection

Configure for single item selection with validation and error handling.

Page Settings
Configure the parent page for this content

Select the parent page where this content should be nested

Documentation

Main documentation section containing all guides and tutorials

pageNavigation

Blog

Blog section with articles and updates

pageContent

Products

Product catalog and information

pageCommerce

About

Company information and team details

pageCompany

Contact

Contact information and support channels

pageSupport

Legal

Legal documents and privacy policy

pagedraftLegal
No parent page selected
0 selected / 1 max
"use client"
 
import { useState } from "react"
import { Relationship, type RelationshipItem } from "@/components/ui/relationship"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
 
const parentPages: RelationshipItem[] = [
  {
    id: 1,
    title: "Documentation",
    type: "page",
    status: "publish",
    taxonomy: ["Navigation"],
    excerpt: "Main documentation section containing all guides and tutorials",
    date: "2024-01-01",
    url: "/docs",
  },
  {
    id: 2,
    title: "Blog",
    type: "page",
    status: "publish",
    taxonomy: ["Content"],
    excerpt: "Blog section with articles and updates",
    date: "2024-01-01",
    url: "/blog",
  },
  {
    id: 3,
    title: "Products",
    type: "page",
    status: "publish",
    taxonomy: ["Commerce"],
    excerpt: "Product catalog and information",
    date: "2024-01-01",
    url: "/products",
  },
  {
    id: 4,
    title: "About",
    type: "page",
    status: "publish",
    taxonomy: ["Company"],
    excerpt: "Company information and team details",
    date: "2024-01-01",
    url: "/about",
  },
  {
    id: 5,
    title: "Contact",
    type: "page",
    status: "publish",
    taxonomy: ["Support"],
    excerpt: "Contact information and support channels",
    date: "2024-01-01",
    url: "/contact",
  },
  {
    id: 6,
    title: "Legal",
    type: "page",
    status: "draft",
    taxonomy: ["Legal"],
    excerpt: "Legal documents and privacy policy",
    date: "2024-01-01",
    url: "/legal",
  },
]
 
export function RelationshipSingleDemo() {
  const [selectedParent, setSelectedParent] = useState<RelationshipItem[]>([])
  const [error, setError] = useState<string>("")
  const [submitted, setSubmitted] = useState(false)
 
  const handleSubmit = () => {
    setSubmitted(true)
    if (selectedParent.length === 0) {
      setError("Please select a parent page")
      return
    }
    setError("")
    const firstParent = selectedParent[0]
    if (firstParent) {
      alert(`Form submitted with parent page: ${firstParent.title}`)
    }
  }
 
  const handleChange = (items: RelationshipItem[] | string[] | number[]) => {
    // Since we're using returnFormat="object" (default), items will be RelationshipItem[]
    const relationshipItems = items as RelationshipItem[]
    setSelectedParent(relationshipItems)
    if (submitted && relationshipItems.length > 0) {
      setError("")
    }
  }
 
  const selectedPage = selectedParent.length > 0 ? selectedParent[0] : null
 
  return (
    <div className="w-full max-w-4xl mx-auto space-y-6">
      <Card>
        <CardHeader>
          <CardTitle>Page Settings</CardTitle>
          <CardDescription>
            Configure the parent page for this content
          </CardDescription>
        </CardHeader>
        <CardContent className="space-y-4">
          <div>
            <label className="text-sm font-medium mb-2 block">
              Parent Page <span className="text-destructive">*</span>
            </label>
            <Relationship
              items={parentPages}
              selectedItems={selectedParent}
              onChange={handleChange}
              multiple={false}
              max={1}
              required={true}
              postTypes={["page"]}
              taxonomies={["Navigation", "Content", "Commerce", "Company", "Support", "Legal"]}
              showFilters={["search", "taxonomy"]}
              showExcerpt={true}
              instructions="Select the parent page where this content should be nested"
              searchPlaceholder="Search parent pages..."
              noItemsText="No parent pages found"
              noSelectedText="No parent page selected"
              error={error}
              variant={error ? "error" : "default"}
            />
          </div>
 
          <div className="flex gap-2">
            <Button onClick={handleSubmit}>
              Save Settings
            </Button>
            <Button 
              variant="outline" 
              onClick={() => {
                setSelectedParent([])
                setError("")
                setSubmitted(false)
              }}
            >
              Reset
            </Button>
          </div>
        </CardContent>
      </Card>
 
      {selectedPage && (
        <Card>
          <CardHeader>
            <CardTitle>Selected Parent Page</CardTitle>
          </CardHeader>
          <CardContent>
            <div className="flex items-center gap-4 p-4 bg-muted/30 rounded-lg">
              <div className="flex-1">
                <h4 className="font-medium">{selectedPage.title}</h4>
                <p className="text-sm text-muted-foreground mt-1">
                  {selectedPage.excerpt}
                </p>
                <div className="flex items-center gap-2 mt-2">
                  <span className="text-xs bg-primary/10 text-primary px-2 py-1 rounded">
                    {selectedPage.type}
                  </span>
                  <span className="text-xs text-muted-foreground">
                    {selectedPage.url}
                  </span>
                </div>
              </div>
            </div>
          </CardContent>
        </Card>
      )}
    </div>
  )
} 

Advanced Configuration

Comprehensive example showcasing all relationship features with dynamic settings.

Advanced Relationship Configuration
Comprehensive example showcasing all relationship features

Select content items for your collection. Use filters to narrow down results.

React Performance Optimization

Learn advanced techniques to optimize React applications for better performance and user experience...

postReactPerformanceJavaScript

TypeScript Best Practices

Comprehensive guide to TypeScript best practices and coding patterns for enterprise applications...

postTypeScriptBest PracticesDevelopment

CSS Grid Mastery

Master CSS Grid layout with practical examples and real-world use cases...

tutorialCSSGridLayout

Next.js 14 Features

Explore the latest features in Next.js 14 and how to use them in your projects...

guideNext.jsReactFramework

Database Optimization

Optimize database queries and improve application performance with these proven techniques...

guidedraftDatabasePerformanceSQL

Microservices Architecture

Build scalable microservices architecture with Docker and Kubernetes...

tutorialArchitectureMicroservicesBackend

GraphQL API Design

Design efficient GraphQL APIs with proper schema design and resolver patterns...

postGraphQLAPIBackend

Testing Strategies

Comprehensive testing strategies for modern web applications...

guideTestingQADevelopment

DevOps Fundamentals

Learn DevOps fundamentals including CI/CD pipelines and infrastructure as code...

tutorialprivateDevOpsCI/CDInfrastructure

Security Best Practices

Essential security best practices for web applications and APIs...

guideSecurityWeb SecurityBest Practices
No items selected
Minimum 2 items required
0 selected / 5 max
"use client"
 
import { useState } from "react"
import { Relationship, type RelationshipItem } from "@/components/ui/relationship"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Badge } from "@/components/ui/badge"
import { Switch } from "@/components/ui/switch"
import { Label } from "@/components/ui/label"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
 
const fullDataset: RelationshipItem[] = [
  {
    id: 1,
    title: "React Performance Optimization",
    type: "post",
    status: "publish",
    taxonomy: ["React", "Performance", "JavaScript"],
    excerpt: "Learn advanced techniques to optimize React applications for better performance and user experience...",
    date: "2024-01-15",
    featured_image: "https://picsum.photos/400/300?random=1",
    url: "/posts/react-performance",
  },
  {
    id: 2,
    title: "TypeScript Best Practices",
    type: "post",
    status: "publish",
    taxonomy: ["TypeScript", "Best Practices", "Development"],
    excerpt: "Comprehensive guide to TypeScript best practices and coding patterns for enterprise applications...",
    date: "2024-01-10",
    featured_image: "https://picsum.photos/400/300?random=2",
    url: "/posts/typescript-best-practices",
  },
  {
    id: 3,
    title: "CSS Grid Mastery",
    type: "tutorial",
    status: "publish",
    taxonomy: ["CSS", "Grid", "Layout"],
    excerpt: "Master CSS Grid layout with practical examples and real-world use cases...",
    date: "2024-01-05",
    featured_image: "https://picsum.photos/400/300?random=3",
    url: "/tutorials/css-grid",
  },
  {
    id: 4,
    title: "Next.js 14 Features",
    type: "guide",
    status: "publish",
    taxonomy: ["Next.js", "React", "Framework"],
    excerpt: "Explore the latest features in Next.js 14 and how to use them in your projects...",
    date: "2024-01-20",
    featured_image: "https://picsum.photos/400/300?random=4",
    url: "/guides/nextjs-14",
  },
  {
    id: 5,
    title: "Database Optimization",
    type: "guide",
    status: "draft",
    taxonomy: ["Database", "Performance", "SQL"],
    excerpt: "Optimize database queries and improve application performance with these proven techniques...",
    date: "2024-01-12",
    featured_image: "https://picsum.photos/400/300?random=5",
    url: "/guides/database-optimization",
  },
  {
    id: 6,
    title: "Microservices Architecture",
    type: "tutorial",
    status: "publish",
    taxonomy: ["Architecture", "Microservices", "Backend"],
    excerpt: "Build scalable microservices architecture with Docker and Kubernetes...",
    date: "2024-01-08",
    featured_image: "https://picsum.photos/400/300?random=6",
    url: "/tutorials/microservices",
  },
  {
    id: 7,
    title: "GraphQL API Design",
    type: "post",
    status: "publish",
    taxonomy: ["GraphQL", "API", "Backend"],
    excerpt: "Design efficient GraphQL APIs with proper schema design and resolver patterns...",
    date: "2024-01-25",
    featured_image: "https://picsum.photos/400/300?random=7",
    url: "/posts/graphql-api",
  },
  {
    id: 8,
    title: "Testing Strategies",
    type: "guide",
    status: "publish",
    taxonomy: ["Testing", "QA", "Development"],
    excerpt: "Comprehensive testing strategies for modern web applications...",
    date: "2024-01-18",
    featured_image: "https://picsum.photos/400/300?random=8",
    url: "/guides/testing-strategies",
  },
  {
    id: 9,
    title: "DevOps Fundamentals",
    type: "tutorial",
    status: "private",
    taxonomy: ["DevOps", "CI/CD", "Infrastructure"],
    excerpt: "Learn DevOps fundamentals including CI/CD pipelines and infrastructure as code...",
    date: "2024-01-03",
    featured_image: "https://picsum.photos/400/300?random=9",
    url: "/tutorials/devops",
  },
  {
    id: 10,
    title: "Security Best Practices",
    type: "guide",
    status: "publish",
    taxonomy: ["Security", "Web Security", "Best Practices"],
    excerpt: "Essential security best practices for web applications and APIs...",
    date: "2024-01-14",
    featured_image: "https://picsum.photos/400/300?random=10",
    url: "/guides/security",
  },
]
 
export function RelationshipAdvancedDemo() {
  const [selectedItems, setSelectedItems] = useState<RelationshipItem[]>([])
  const [returnFormat, setReturnFormat] = useState<"object" | "id">("object")
  const [showAllFeatures, setShowAllFeatures] = useState(false)
  const [minItems, setMinItems] = useState(2)
  const [maxItems, setMaxItems] = useState(5)
  const [isRequired, setIsRequired] = useState(true)
  const [error, setError] = useState<string>("")
 
  const handleChange = (items: RelationshipItem[] | string[] | number[]) => {
    if (returnFormat === "object") {
      setSelectedItems(items as RelationshipItem[])
    } else {
      // Convert IDs back to objects for display
      const ids = items as (string | number)[]
      const objects = fullDataset.filter(item => ids.includes(item.id))
      setSelectedItems(objects)
    }
    
    // Clear error when valid selection is made
    if (isRequired && (items as any[]).length >= minItems) {
      setError("")
    }
  }
 
  const handleValidate = () => {
    if (isRequired && selectedItems.length < minItems) {
      setError(`Minimum ${minItems} items required`)
      return false
    }
    setError("")
    return true
  }
 
  const handleSave = () => {
    if (handleValidate()) {
      alert(`Saved ${selectedItems.length} items with return format: ${returnFormat}`)
    }
  }
 
  return (
    <div className="w-full max-w-6xl mx-auto space-y-6">
      <Card>
        <CardHeader>
          <CardTitle>Advanced Relationship Configuration</CardTitle>
          <CardDescription>
            Comprehensive example showcasing all relationship features
          </CardDescription>
        </CardHeader>
        <CardContent>
          <Tabs defaultValue="relationship" className="w-full">
            <TabsList className="grid w-full grid-cols-3">
              <TabsTrigger value="relationship">Relationship</TabsTrigger>
              <TabsTrigger value="settings">Settings</TabsTrigger>
              <TabsTrigger value="output">Output</TabsTrigger>
            </TabsList>
            
            <TabsContent value="relationship" className="space-y-4">
              <div className="flex items-center space-x-2 mb-4">
                <Switch
                  id="all-features"
                  checked={showAllFeatures}
                  onCheckedChange={setShowAllFeatures}
                />
                <Label htmlFor="all-features">Show all display features</Label>
              </div>
 
              <Relationship
                items={fullDataset}
                selectedItems={selectedItems}
                onChange={handleChange}
                multiple={true}
                min={minItems}
                max={maxItems}
                required={isRequired}
                returnFormat={returnFormat}
                postTypes={["post", "guide", "tutorial"]}
                postStatuses={["publish", "draft", "private"]}
                taxonomies={["React", "TypeScript", "CSS", "Next.js", "Database", "GraphQL", "Testing", "Security"]}
                showFilters={true}
                showFeaturedImage={showAllFeatures}
                showExcerpt={true}
                showDate={showAllFeatures}
                showPreview={showAllFeatures}
                sortable={showAllFeatures}
                instructions="Select content items for your collection. Use filters to narrow down results."
                searchPlaceholder="Search by title, content, or tags..."
                error={error}
                variant={error ? "error" : "default"}
              />
 
              <div className="flex gap-2">
                <Button onClick={handleSave}>
                  Save Selection
                </Button>
                <Button 
                  variant="outline" 
                  onClick={() => {
                    setSelectedItems([])
                    setError("")
                  }}
                >
                  Clear All
                </Button>
                <Button 
                  variant="outline" 
                  onClick={handleValidate}
                >
                  Validate
                </Button>
              </div>
            </TabsContent>
 
            <TabsContent value="settings" className="space-y-4">
              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                <div className="space-y-4">
                  <div>
                    <Label htmlFor="return-format">Return Format</Label>
                    <select
                      id="return-format"
                      value={returnFormat}
                      onChange={(e) => setReturnFormat(e.target.value as "object" | "id")}
                      className="w-full mt-1 p-2 border rounded"
                    >
                      <option value="object">Object Array</option>
                      <option value="id">ID Array</option>
                    </select>
                  </div>
 
                  <div>
                    <Label htmlFor="min-items">Minimum Items</Label>
                    <input
                      id="min-items"
                      type="number"
                      min="0"
                      max="10"
                      value={minItems}
                      onChange={(e) => setMinItems(parseInt(e.target.value))}
                      className="w-full mt-1 p-2 border rounded"
                    />
                  </div>
 
                  <div>
                    <Label htmlFor="max-items">Maximum Items</Label>
                    <input
                      id="max-items"
                      type="number"
                      min="1"
                      max="20"
                      value={maxItems}
                      onChange={(e) => setMaxItems(parseInt(e.target.value))}
                      className="w-full mt-1 p-2 border rounded"
                    />
                  </div>
                </div>
 
                <div className="space-y-4">
                  <div className="flex items-center space-x-2">
                    <Switch
                      id="required"
                      checked={isRequired}
                      onCheckedChange={setIsRequired}
                    />
                    <Label htmlFor="required">Required Field</Label>
                  </div>
 
                  <div className="flex items-center space-x-2">
                    <Switch
                      id="features"
                      checked={showAllFeatures}
                      onCheckedChange={setShowAllFeatures}
                    />
                    <Label htmlFor="features">Enhanced Display</Label>
                  </div>
 
                  <div className="p-3 bg-muted/30 rounded">
                    <h4 className="font-medium text-sm mb-2">Current Settings</h4>
                    <div className="space-y-1 text-xs">
                      <div>Min: {minItems}, Max: {maxItems}</div>
                      <div>Required: {isRequired ? 'Yes' : 'No'}</div>
                      <div>Return: {returnFormat}</div>
                      <div>Features: {showAllFeatures ? 'All' : 'Basic'}</div>
                    </div>
                  </div>
                </div>
              </div>
            </TabsContent>
 
            <TabsContent value="output" className="space-y-4">
              <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
                <div>
                  <h4 className="font-medium mb-2">Selected Items ({selectedItems.length})</h4>
                  <div className="space-y-2 max-h-80 overflow-y-auto">
                    {selectedItems.map((item) => (
                      <div key={item.id} className="flex items-center gap-3 p-2 border rounded">
                        {showAllFeatures && item.featured_image && (
                          <img
                            src={item.featured_image}
                            alt={item.title}
                            className="w-10 h-10 rounded object-cover"
                          />
                        )}
                        <div className="flex-1">
                          <div className="font-medium text-sm">{item.title}</div>
                          <div className="flex items-center gap-2 mt-1">
                            <Badge variant="outline" className="text-xs">
                              {item.type}
                            </Badge>
                            <Badge variant="secondary" className="text-xs">
                              {item.status}
                            </Badge>
                          </div>
                        </div>
                      </div>
                    ))}
                    {selectedItems.length === 0 && (
                      <p className="text-muted-foreground text-sm text-center py-8">
                        No items selected
                      </p>
                    )}
                  </div>
                </div>
 
                <div>
                  <h4 className="font-medium mb-2">Raw Output</h4>
                  <div className="bg-muted/30 p-3 rounded text-xs font-mono max-h-80 overflow-y-auto">
                    <pre>
                      {JSON.stringify(
                        returnFormat === "object" 
                          ? selectedItems.map(item => ({
                              id: item.id,
                              title: item.title,
                              type: item.type,
                              status: item.status
                            }))
                          : selectedItems.map(item => item.id),
                        null,
                        2
                      )}
                    </pre>
                  </div>
                </div>
              </div>
            </TabsContent>
          </Tabs>
        </CardContent>
      </Card>
    </div>
  )
} 

Data Structure

RelationshipItem

The component expects items to follow this structure:

interface RelationshipItem {
  id: string | number          // Unique identifier
  title: string               // Display title
  type?: string              // Content type (post, page, etc.)
  status?: "publish" | "draft" | "private"  // Publication status
  taxonomy?: string[]        // Categories, tags, etc.
  featured_image?: string    // Image URL
  excerpt?: string          // Short description
  date?: string            // Publication date
  url?: string            // Preview URL
}

Example Data

const posts: RelationshipItem[] = [
  {
    id: 1,
    title: "Introduction to React Hooks",
    type: "post",
    status: "publish",
    taxonomy: ["React", "JavaScript"],
    excerpt: "Learn the fundamentals of React Hooks...",
    date: "2024-01-15",
    featured_image: "https://example.com/image.jpg",
    url: "/posts/react-hooks"
  },
  // ... more items
]

Async Loading

For large datasets, use the onLoad prop to fetch data asynchronously:

const fetchPosts = async (filter: RelationshipFilter): Promise<RelationshipItem[]> => {
  const response = await fetch('/api/posts', {
    method: 'POST',
    body: JSON.stringify(filter)
  })
  return response.json()
}

<Relationship
  onLoad={fetchPosts}
  selectedItems={selected}
  onChange={setSelected}
  postTypes={["post", "page"]}
  taxonomies={["category", "tag"]}
/>

The onLoad function receives a filter object with:

interface RelationshipFilter {
  search?: string           // Search query
  post_type?: string[]     // Filter by post types
  post_status?: string[]   // Filter by post statuses  
  taxonomy?: string[]      // Filter by taxonomies
}

Filtering Options

Control which filters are displayed using the showFilters prop:

// Show all filters (default)
<Relationship showFilters={true} />

// Show only search
<Relationship showFilters={["search"]} />

// Show search and post type filters
<Relationship showFilters={["search", "post_type"]} />

// Hide all filters
<Relationship showFilters={false} />

Display Options

Customize the item display with various options:

<Relationship
  showExcerpt={true}          // Show item excerpts
  showDate={true}             // Show publication dates
  showFeaturedImage={true}    // Show featured images
  showPreview={true}          // Show preview links
  sortable={true}             // Show drag handles (UI ready)
/>

Return Formats

Choose how selected items are returned:

// Return full objects (default)
<Relationship
  returnFormat="object"
  onChange={(items) => {
    // items: RelationshipItem[]
    console.log(items)
  }}
/>

// Return only IDs
<Relationship
  returnFormat="id"
  onChange={(items) => {
    // items: (string | number)[]
    console.log(items) // [1, 2, 3]
  }}
/>

Validation

Add validation rules for selection:

<Relationship
  required={true}           // Field is required
  min={1}                  // Minimum selections
  max={5}                  // Maximum selections
  error="Please select at least one item"
  variant="error"          // Error styling
/>

API Reference

Relationship

PropTypeDefault
size?
"sm" | "md" | "lg" | null
-
variant?
"default" | "error" | null
-
onTransitionStartCapture?
TransitionEventHandler<HTMLDivElement>
-
onTransitionStart?
TransitionEventHandler<HTMLDivElement>
-
onTransitionRunCapture?
TransitionEventHandler<HTMLDivElement>
-
onTransitionRun?
TransitionEventHandler<HTMLDivElement>
-
onTransitionEndCapture?
TransitionEventHandler<HTMLDivElement>
-
onTransitionEnd?
TransitionEventHandler<HTMLDivElement>
-
onTransitionCancelCapture?
TransitionEventHandler<HTMLDivElement>
-
onTransitionCancel?
TransitionEventHandler<HTMLDivElement>
-
onBeforeToggle?
ToggleEventHandler<HTMLDivElement>
-
onToggle?
ToggleEventHandler<HTMLDivElement>
-
onAnimationIterationCapture?
AnimationEventHandler<HTMLDivElement>
-
onAnimationIteration?
AnimationEventHandler<HTMLDivElement>
-
onAnimationEndCapture?
AnimationEventHandler<HTMLDivElement>
-
onAnimationEnd?
AnimationEventHandler<HTMLDivElement>
-
onAnimationStartCapture?
AnimationEventHandler<HTMLDivElement>
-
onAnimationStart?
AnimationEventHandler<HTMLDivElement>
-
onWheelCapture?
WheelEventHandler<HTMLDivElement>
-
onWheel?
WheelEventHandler<HTMLDivElement>
-
onScrollEndCapture?
UIEventHandler<HTMLDivElement>
-
onScrollEnd?
UIEventHandler<HTMLDivElement>
-
onScrollCapture?
UIEventHandler<HTMLDivElement>
-
onScroll?
UIEventHandler<HTMLDivElement>
-
onLostPointerCaptureCapture?
PointerEventHandler<HTMLDivElement>
-
onLostPointerCapture?
PointerEventHandler<HTMLDivElement>
-
onGotPointerCaptureCapture?
PointerEventHandler<HTMLDivElement>
-
onGotPointerCapture?
PointerEventHandler<HTMLDivElement>
-
onPointerOutCapture?
PointerEventHandler<HTMLDivElement>
-
onPointerOut?
PointerEventHandler<HTMLDivElement>
-
onPointerOverCapture?
PointerEventHandler<HTMLDivElement>
-
onPointerOver?
PointerEventHandler<HTMLDivElement>
-
onPointerLeave?
PointerEventHandler<HTMLDivElement>
-
onPointerEnter?
PointerEventHandler<HTMLDivElement>
-
onPointerCancelCapture?
PointerEventHandler<HTMLDivElement>
-
onPointerCancel?
PointerEventHandler<HTMLDivElement>
-
onPointerUpCapture?
PointerEventHandler<HTMLDivElement>
-
onPointerUp?
PointerEventHandler<HTMLDivElement>
-
onPointerMoveCapture?
PointerEventHandler<HTMLDivElement>
-
onPointerMove?
PointerEventHandler<HTMLDivElement>
-
onPointerDownCapture?
PointerEventHandler<HTMLDivElement>
-
onPointerDown?
PointerEventHandler<HTMLDivElement>
-
onTouchStartCapture?
TouchEventHandler<HTMLDivElement>
-
onTouchStart?
TouchEventHandler<HTMLDivElement>
-
onTouchMoveCapture?
TouchEventHandler<HTMLDivElement>
-
onTouchMove?
TouchEventHandler<HTMLDivElement>
-
onTouchEndCapture?
TouchEventHandler<HTMLDivElement>
-
onTouchEnd?
TouchEventHandler<HTMLDivElement>
-
onTouchCancelCapture?
TouchEventHandler<HTMLDivElement>
-
onTouchCancel?
TouchEventHandler<HTMLDivElement>
-
onSelectCapture?
ReactEventHandler<HTMLDivElement>
-
onMouseUpCapture?
MouseEventHandler<HTMLDivElement>
-
onMouseUp?
MouseEventHandler<HTMLDivElement>
-
onMouseOverCapture?
MouseEventHandler<HTMLDivElement>
-
onMouseOver?
MouseEventHandler<HTMLDivElement>
-
onMouseOutCapture?
MouseEventHandler<HTMLDivElement>
-
onMouseOut?
MouseEventHandler<HTMLDivElement>
-
onMouseMoveCapture?
MouseEventHandler<HTMLDivElement>
-
onMouseMove?
MouseEventHandler<HTMLDivElement>
-
onMouseLeave?
MouseEventHandler<HTMLDivElement>
-
onMouseEnter?
MouseEventHandler<HTMLDivElement>
-
onMouseDownCapture?
MouseEventHandler<HTMLDivElement>
-
onMouseDown?
MouseEventHandler<HTMLDivElement>
-
onDropCapture?
DragEventHandler<HTMLDivElement>
-
onDrop?
DragEventHandler<HTMLDivElement>
-
onDragStartCapture?
DragEventHandler<HTMLDivElement>
-
onDragStart?
DragEventHandler<HTMLDivElement>
-
onDragOverCapture?
DragEventHandler<HTMLDivElement>
-
onDragOver?
DragEventHandler<HTMLDivElement>
-
onDragLeaveCapture?
DragEventHandler<HTMLDivElement>
-
onDragLeave?
DragEventHandler<HTMLDivElement>
-
onDragExitCapture?
DragEventHandler<HTMLDivElement>
-
onDragExit?
DragEventHandler<HTMLDivElement>
-
onDragEnterCapture?
DragEventHandler<HTMLDivElement>
-
onDragEnter?
DragEventHandler<HTMLDivElement>
-
onDragEndCapture?
DragEventHandler<HTMLDivElement>
-
onDragEnd?
DragEventHandler<HTMLDivElement>
-
onDragCapture?
DragEventHandler<HTMLDivElement>
-
onDrag?
DragEventHandler<HTMLDivElement>
-
onDoubleClickCapture?
MouseEventHandler<HTMLDivElement>
-
onDoubleClick?
MouseEventHandler<HTMLDivElement>
-
onContextMenuCapture?
MouseEventHandler<HTMLDivElement>
-
onContextMenu?
MouseEventHandler<HTMLDivElement>
-
onClickCapture?
MouseEventHandler<HTMLDivElement>
-
onClick?
MouseEventHandler<HTMLDivElement>
-
onAuxClickCapture?
MouseEventHandler<HTMLDivElement>
-
onAuxClick?
MouseEventHandler<HTMLDivElement>
-
onWaitingCapture?
ReactEventHandler<HTMLDivElement>
-
onWaiting?
ReactEventHandler<HTMLDivElement>
-
onVolumeChangeCapture?
ReactEventHandler<HTMLDivElement>
-
onVolumeChange?
ReactEventHandler<HTMLDivElement>
-
onTimeUpdateCapture?
ReactEventHandler<HTMLDivElement>
-
onTimeUpdate?
ReactEventHandler<HTMLDivElement>
-
onSuspendCapture?
ReactEventHandler<HTMLDivElement>
-
onSuspend?
ReactEventHandler<HTMLDivElement>
-
onStalledCapture?
ReactEventHandler<HTMLDivElement>
-
onStalled?
ReactEventHandler<HTMLDivElement>
-
onSeekingCapture?
ReactEventHandler<HTMLDivElement>
-
onSeeking?
ReactEventHandler<HTMLDivElement>
-
onSeekedCapture?
ReactEventHandler<HTMLDivElement>
-
onSeeked?
ReactEventHandler<HTMLDivElement>
-
onRateChangeCapture?
ReactEventHandler<HTMLDivElement>
-
onRateChange?
ReactEventHandler<HTMLDivElement>
-
onProgressCapture?
ReactEventHandler<HTMLDivElement>
-
onProgress?
ReactEventHandler<HTMLDivElement>
-
onPlayingCapture?
ReactEventHandler<HTMLDivElement>
-
onPlaying?
ReactEventHandler<HTMLDivElement>
-
onPlayCapture?
ReactEventHandler<HTMLDivElement>
-
onPlay?
ReactEventHandler<HTMLDivElement>
-
onPauseCapture?
ReactEventHandler<HTMLDivElement>
-
onPause?
ReactEventHandler<HTMLDivElement>
-
onLoadStartCapture?
ReactEventHandler<HTMLDivElement>
-
onLoadStart?
ReactEventHandler<HTMLDivElement>
-
onLoadedMetadataCapture?
ReactEventHandler<HTMLDivElement>
-
onLoadedMetadata?
ReactEventHandler<HTMLDivElement>
-
onLoadedDataCapture?
ReactEventHandler<HTMLDivElement>
-
onLoadedData?
ReactEventHandler<HTMLDivElement>
-
onEndedCapture?
ReactEventHandler<HTMLDivElement>
-
onEnded?
ReactEventHandler<HTMLDivElement>
-
onEncryptedCapture?
ReactEventHandler<HTMLDivElement>
-
onEncrypted?
ReactEventHandler<HTMLDivElement>
-
onEmptiedCapture?
ReactEventHandler<HTMLDivElement>
-
onEmptied?
ReactEventHandler<HTMLDivElement>
-
onDurationChangeCapture?
ReactEventHandler<HTMLDivElement>
-
onDurationChange?
ReactEventHandler<HTMLDivElement>
-
onCanPlayThroughCapture?
ReactEventHandler<HTMLDivElement>
-
onCanPlayThrough?
ReactEventHandler<HTMLDivElement>
-
onCanPlayCapture?
ReactEventHandler<HTMLDivElement>
-
onCanPlay?
ReactEventHandler<HTMLDivElement>
-
onAbortCapture?
ReactEventHandler<HTMLDivElement>
-
onAbort?
ReactEventHandler<HTMLDivElement>
-
onKeyUpCapture?
KeyboardEventHandler<HTMLDivElement>
-
onKeyUp?
KeyboardEventHandler<HTMLDivElement>
-
onKeyDownCapture?
KeyboardEventHandler<HTMLDivElement>
-
onKeyDown?
KeyboardEventHandler<HTMLDivElement>
-
onErrorCapture?
ReactEventHandler<HTMLDivElement>
-
onError?
ReactEventHandler<HTMLDivElement>
-
onLoadCapture?
ReactEventHandler<HTMLDivElement>
-
onInvalidCapture?
FormEventHandler<HTMLDivElement>
-
onInvalid?
FormEventHandler<HTMLDivElement>
-
onSubmitCapture?
FormEventHandler<HTMLDivElement>
-
onSubmit?
FormEventHandler<HTMLDivElement>
-
onResetCapture?
FormEventHandler<HTMLDivElement>
-
onReset?
FormEventHandler<HTMLDivElement>
-
onInputCapture?
FormEventHandler<HTMLDivElement>
-
onInput?
FormEventHandler<HTMLDivElement>
-
onBeforeInputCapture?
FormEventHandler<HTMLDivElement>
-
onBeforeInput?
InputEventHandler<HTMLDivElement>
-
onChangeCapture?
FormEventHandler<HTMLDivElement>
-
onBlurCapture?
FocusEventHandler<HTMLDivElement>
-
onBlur?
FocusEventHandler<HTMLDivElement>
-
onFocusCapture?
FocusEventHandler<HTMLDivElement>
-
onFocus?
FocusEventHandler<HTMLDivElement>
-
onCompositionUpdateCapture?
CompositionEventHandler<HTMLDivElement>
-
onCompositionUpdate?
CompositionEventHandler<HTMLDivElement>
-
onCompositionStartCapture?
CompositionEventHandler<HTMLDivElement>
-
onCompositionStart?
CompositionEventHandler<HTMLDivElement>
-
onCompositionEndCapture?
CompositionEventHandler<HTMLDivElement>
-
onCompositionEnd?
CompositionEventHandler<HTMLDivElement>
-
onPasteCapture?
ClipboardEventHandler<HTMLDivElement>
-
onPaste?
ClipboardEventHandler<HTMLDivElement>
-
onCutCapture?
ClipboardEventHandler<HTMLDivElement>
-
onCut?
ClipboardEventHandler<HTMLDivElement>
-
onCopyCapture?
ClipboardEventHandler<HTMLDivElement>
-
onCopy?
ClipboardEventHandler<HTMLDivElement>
-
dangerouslySetInnerHTML?
{ __html: string | TrustedHTML; }
-
children?
ReactNode
-
aria-valuetext?
string
-
aria-valuenow?
number
-
aria-valuemin?
number
-
aria-valuemax?
number
-
aria-sort?
"none" | "ascending" | "descending" | "other"
-
aria-setsize?
number
-
aria-selected?
Booleanish
-
aria-rowspan?
number
-
aria-rowindextext?
string
-
aria-rowindex?
number
-
aria-rowcount?
number
-
aria-roledescription?
string
-
aria-required?
Booleanish
-
aria-relevant?
"text" | "additions" | "additions removals" | "additions text" | "all" | "removals" | "removals additions" | "removals text" | "text additions" | "text removals"
-
aria-readonly?
Booleanish
-
aria-pressed?
boolean | "true" | "false" | "mixed"
-
aria-posinset?
number
-
aria-placeholder?
string
-
aria-owns?
string
-
aria-orientation?
"horizontal" | "vertical"
-
aria-multiselectable?
Booleanish
-
aria-multiline?
Booleanish
-
aria-modal?
Booleanish
-
aria-live?
"off" | "assertive" | "polite"
-
aria-level?
number
-
aria-labelledby?
string
-
aria-label?
string
-
aria-keyshortcuts?
string
-
aria-invalid?
boolean | "true" | "false" | "grammar" | "spelling"
-
aria-hidden?
Booleanish
-
aria-haspopup?
boolean | "true" | "false" | "dialog" | "grid" | "listbox" | "menu" | "tree"
-
aria-flowto?
string
-
aria-expanded?
Booleanish
-
aria-errormessage?
string
-
aria-disabled?
Booleanish
-
aria-details?
string
-
aria-description?
string
-
aria-describedby?
string
-
aria-current?
boolean | "true" | "false" | "page" | "step" | "location" | "date" | "time"
-
aria-controls?
string
-
aria-colspan?
number
-
aria-colindextext?
string
-
aria-colindex?
number
-
aria-colcount?
number
-
aria-checked?
boolean | "true" | "false" | "mixed"
-
aria-busy?
Booleanish
-
aria-brailleroledescription?
string
-
aria-braillelabel?
string
-
aria-autocomplete?
"none" | "list" | "inline" | "both"
-
aria-atomic?
Booleanish
-
aria-activedescendant?
string
-
part?
string
-
exportparts?
string
-
is?
string
-
inputMode?
"none" | "search" | "text" | "tel" | "url" | "email" | "numeric" | "decimal"
-
inert?
boolean
-
popoverTarget?
string
-
popoverTargetAction?
"toggle" | "show" | "hide"
-
popover?
"" | "auto" | "manual"
-
unselectable?
"off" | "on"
-
security?
string
-
results?
number
-
itemRef?
string
-
itemID?
string
-
itemType?
string
-
itemScope?
boolean
-
itemProp?
string
-
color?
string
-
autoSave?
string
-
autoCorrect?
string
-
vocab?
string
-
typeof?
string
-
rev?
string
-
resource?
string
-
rel?
string
-
property?
string
-
prefix?
string
-
inlist?
any
-
datatype?
string
-
content?
string
-
about?
string
-
role?
AriaRole
-
radioGroup?
string
-
translate?
"yes" | "no"
-
title?
string
-
tabIndex?
number
-
style?
CSSProperties
-
spellCheck?
Booleanish
-
slot?
string
-
nonce?
string
-
lang?
string
-
id?
string
-
hidden?
boolean
-
enterKeyHint?
"enter" | "done" | "go" | "next" | "previous" | "search" | "send"
-
draggable?
Booleanish
-
dir?
string
-
contextMenu?
string
-
contentEditable?
Booleanish | "inherit" | "plaintext-only"
-
className?
string
-
autoFocus?
boolean
-
autoCapitalize?
"off" | "none" | "on" | "sentences" | "words" | "characters" | (string & {})
-
accessKey?
string
-
suppressHydrationWarning?
boolean
-
suppressContentEditableWarning?
boolean
-
defaultValue?
string | number | readonly string[]
-
defaultChecked?
boolean
-
error?
string
-
instructions?
string
-
loadingText?
string
-
noSelectedText?
string
-
noItemsText?
string
-
searchPlaceholder?
string
-
onRemove?
((item: RelationshipItem) => void)
-
onSelect?
((item: RelationshipItem) => void)
-
onChange?
((items: RelationshipItem[] | string[] | number[]) => void)
-
returnFormat?
"object" | "id"
-
sortable?
boolean
-
showDate?
boolean
-
showExcerpt?
boolean
-
showFeaturedImage?
boolean
-
showPreview?
boolean
-
showFilters?
boolean | string[]
-
taxonomies?
string[]
-
postStatuses?
string[]
-
postTypes?
string[]
-
loading?
boolean
-
onLoad?
((filter: RelationshipFilter) => Promise<RelationshipItem[]>)
-
required?
boolean
-
max?
number
-
min?
number
-
multiple?
boolean
-
selectedItems?
RelationshipItem[]
-
items?
RelationshipItem[]
-
aria-dropeffect?
"none" | "link" | "copy" | "execute" | "move" | "popup"
-
aria-grabbed?
Booleanish
-
onKeyPress?
KeyboardEventHandler<HTMLDivElement>
-
onKeyPressCapture?
KeyboardEventHandler<HTMLDivElement>
-

RelationshipItem Interface

interface RelationshipItem {
  id: string | number          // Unique identifier
  title: string               // Display title
  type?: string              // Post type (post, page, etc.)
  status?: "publish" | "draft" | "private"  // Publication status
  taxonomy?: string[]        // Categories, tags, etc.
  featured_image?: string    // Image URL
  excerpt?: string          // Short description
  date?: string            // Publication date
  url?: string            // Preview URL
}

RelationshipFilter Interface

interface RelationshipFilter {
  search?: string           // Search query
  post_type?: string[]     // Filter by post types
  post_status?: string[]   // Filter by post statuses
  taxonomy?: string[]      // Filter by taxonomies
}

Event Handlers

PropTypeDescription
onChange(items: RelationshipItem[] | string[] | number[]) => voidCalled when selection changes
onSelect(item: RelationshipItem) => voidCalled when an item is selected
onRemove(item: RelationshipItem) => voidCalled when an item is removed
onLoad(filter: RelationshipFilter) => Promise<RelationshipItem[]>Async data loading function

Configuration Props

PropTypeDefaultDescription
multiplebooleantrueAllow multiple selections
minnumber0Minimum required selections
maxnumber10Maximum allowed selections
requiredbooleanfalseWhether field is required
returnFormat"object" | "id""object"Format of returned data

Display Props

PropTypeDefaultDescription
showExcerptbooleanfalseShow item excerpts
showDatebooleanfalseShow item dates
showFeaturedImagebooleanfalseShow featured images
showPreviewbooleanfalseShow preview links
sortablebooleanfalseShow drag handles for sorting

Filter Props

PropTypeDefaultDescription
postTypesstring[][]Available post types for filtering
postStatusesstring[][]Available post statuses for filtering
taxonomiesstring[][]Available taxonomies for filtering
showFiltersboolean | string[]trueWhich filters to show

Text Props

PropTypeDefaultDescription
searchPlaceholderstring"Search..."Search input placeholder
noItemsTextstring"No items found"Text when no items available
noSelectedTextstring"No items selected"Text when nothing selected
loadingTextstring"Loading..."Loading state text
instructionsstring-Instructions text at top

Styling Props

PropTypeDefaultDescription
variant"default" | "error""default"Visual variant
size"sm" | "md" | "lg""md"Size variant
errorstring-Error message to display

Common Patterns

Content Management System

const [parentPage, setParentPage] = useState(null)

<Relationship
  items={pages}
  selectedItems={parentPage ? [parentPage] : []}
  onChange={(items) => setParentPage(items[0] || null)}
  multiple={false}
  max={1}
  required={true}
  showFilters={["search"]}
  error={errors.parent_page}
/>
const [relatedProducts, setRelatedProducts] = useState([])

<Relationship
  onLoad={fetchProducts}
  selectedItems={relatedProducts}
  onChange={setRelatedProducts}
  postTypes={["product"]}
  taxonomies={["product_cat", "product_tag"]}
  showFeaturedImage={true}
  showExcerpt={true}
  max={8}
  returnFormat="id"
/>

Blog Post Relations

const [relatedPosts, setRelatedPosts] = useState([])

<Relationship
  onLoad={async (filter) => {
    const response = await fetch('/api/posts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(filter)
    })
    return response.json()
  }}
  selectedItems={relatedPosts}
  onChange={setRelatedPosts}
  postTypes={["post", "page", "article"]}
  taxonomies={["category", "tag"]}
  showExcerpt={true}
  showFeaturedImage={true}
  max={5}
/>

Accessibility

The Relationship component follows accessibility best practices:

  • Keyboard Navigation - Full keyboard support for all interactions
  • Screen Reader Support - Proper ARIA labels and descriptions
  • Focus Management - Clear focus indicators and logical tab order
  • High Contrast - Supports high contrast mode
  • Reduced Motion - Respects prefers-reduced-motion settings

Keyboard Interactions

KeyDescription
TabMove focus between interface elements
EnterSelect/deselect items
EscapeClear focus from filters
Arrow KeysNavigate through available items