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
Advanced TypeScript Patterns
Building UIs with Tailwind CSS
State Management Guide
API Design Best Practices
Component Architecture
"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
Advanced TypeScript Patterns
Building UIs with Tailwind CSS
State Management Guide
API Design Best Practices
Component Architecture
"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
"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.
Select the parent page where this content should be nested
Documentation
Main documentation section containing all guides and tutorials
Blog
Blog section with articles and updates
Products
Product catalog and information
About
Company information and team details
Contact
Contact information and support channels
Legal
Legal documents and privacy policy
"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.
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...
TypeScript Best Practices
Comprehensive guide to TypeScript best practices and coding patterns for enterprise applications...
CSS Grid Mastery
Master CSS Grid layout with practical examples and real-world use cases...
Next.js 14 Features
Explore the latest features in Next.js 14 and how to use them in your projects...
Database Optimization
Optimize database queries and improve application performance with these proven techniques...
Microservices Architecture
Build scalable microservices architecture with Docker and Kubernetes...
GraphQL API Design
Design efficient GraphQL APIs with proper schema design and resolver patterns...
Testing Strategies
Comprehensive testing strategies for modern web applications...
DevOps Fundamentals
Learn DevOps fundamentals including CI/CD pipelines and infrastructure as code...
Security Best Practices
Essential security best practices for web applications and APIs...
"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
Prop | Type | Default |
---|---|---|
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
Prop | Type | Description |
---|---|---|
onChange | (items: RelationshipItem[] | string[] | number[]) => void | Called when selection changes |
onSelect | (item: RelationshipItem) => void | Called when an item is selected |
onRemove | (item: RelationshipItem) => void | Called when an item is removed |
onLoad | (filter: RelationshipFilter) => Promise<RelationshipItem[]> | Async data loading function |
Configuration Props
Prop | Type | Default | Description |
---|---|---|---|
multiple | boolean | true | Allow multiple selections |
min | number | 0 | Minimum required selections |
max | number | 10 | Maximum allowed selections |
required | boolean | false | Whether field is required |
returnFormat | "object" | "id" | "object" | Format of returned data |
Display Props
Prop | Type | Default | Description |
---|---|---|---|
showExcerpt | boolean | false | Show item excerpts |
showDate | boolean | false | Show item dates |
showFeaturedImage | boolean | false | Show featured images |
showPreview | boolean | false | Show preview links |
sortable | boolean | false | Show drag handles for sorting |
Filter Props
Prop | Type | Default | Description |
---|---|---|---|
postTypes | string[] | [] | Available post types for filtering |
postStatuses | string[] | [] | Available post statuses for filtering |
taxonomies | string[] | [] | Available taxonomies for filtering |
showFilters | boolean | string[] | true | Which filters to show |
Text Props
Prop | Type | Default | Description |
---|---|---|---|
searchPlaceholder | string | "Search..." | Search input placeholder |
noItemsText | string | "No items found" | Text when no items available |
noSelectedText | string | "No items selected" | Text when nothing selected |
loadingText | string | "Loading..." | Loading state text |
instructions | string | - | Instructions text at top |
Styling Props
Prop | Type | Default | Description |
---|---|---|---|
variant | "default" | "error" | "default" | Visual variant |
size | "sm" | "md" | "lg" | "md" | Size variant |
error | string | - | 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}
/>
E-commerce Related Products
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
Key | Description |
---|---|
Tab | Move focus between interface elements |
Enter | Select/deselect items |
Escape | Clear focus from filters |
Arrow Keys | Navigate through available items |