ACF UI
Components

Post Object

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

Featured Post

Select a post to feature on the homepage

"use client"
 
import { useState } from "react"
import { PostObject, type PostObjectItem } from "@/components/ui/post-object"
 
const posts = [
  {
    id: 1,
    title: "Getting Started with React",
    type: "post",
    status: "publish" as const,
    excerpt: "Learn the fundamentals of React development with this comprehensive guide.",
    date: "2024-01-15",
  },
  {
    id: 2,
    title: "Advanced TypeScript Patterns",
    type: "post", 
    status: "publish" as const,
    excerpt: "Explore advanced TypeScript patterns for better type safety.",
    date: "2024-01-10",
  },
  {
    id: 3,
    title: "Building UIs with Tailwind CSS",
    type: "post",
    status: "draft" as const,
    excerpt: "Create beautiful, responsive interfaces with Tailwind CSS.",
    date: "2024-01-05",
  },
  {
    id: 4,
    title: "State Management Guide",
    type: "guide",
    status: "publish" as const,
    excerpt: "Complete guide to managing state in modern React applications.",
    date: "2024-01-20",
  },
  {
    id: 5,
    title: "API Design Best Practices",
    type: "guide",
    status: "publish" as const,
    excerpt: "Design robust APIs that scale with your application needs.",
    date: "2024-01-12",
  },
] satisfies PostObjectItem[]
 
export function PostObjectDemo() {
  const [selectedPost, setSelectedPost] = useState<PostObjectItem | null>(null)
 
  return (
    <div className="w-full max-w-2xl space-y-4">
      <div>
        <h3 className="text-lg font-semibold">Featured Post</h3>
        <p className="text-sm text-muted-foreground">
          Select a post to feature on the homepage
        </p>
      </div>
 
      <PostObject
        items={posts}
        selectedItems={selectedPost ? [selectedPost] : []}
        onChange={(items) => {
          const postItems = items as PostObjectItem[]
          setSelectedPost(postItems.length > 0 ? postItems[0] || null : null)
        }}
        multiple={false}
        allowNull={true}
        postTypes={[]}
        postStatuses={["publish", "draft"]}
        showFilters={["search", "post_type"]}
        placeholder="Select a featured post..."
        searchPlaceholder="Search posts..."
        showExcerpt={true}
        showDate={true}
      />
 
      {selectedPost && (
        <div className="mt-6 p-4 bg-muted/30 rounded-lg">
          <h4 className="font-medium">Selected Post:</h4>
          <div className="mt-2">
            <div className="font-medium">{selectedPost.title}</div>
            <div className="text-sm text-muted-foreground mt-1">
              {selectedPost.excerpt}
            </div>
            <div className="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
              <span className="capitalize">{selectedPost.type}</span>
              <span>•</span>
              <span>{selectedPost.date}</span>
              <span>•</span>
              <span className="capitalize">{selectedPost.status}</span>
            </div>
          </div>
        </div>
      )}
    </div>
  )
} 

Installation

CLI

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

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 {
  Check,
  ChevronDown,
  Eye,
  GripVertical,
  X,
} from "lucide-react"
 
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover"
import {
  Command,
  CommandEmpty,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
} from "@/components/ui/command"
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select"
 
const postObjectVariants = cva(
  "relative w-full rounded-md border border-input bg-background text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
  {
    variants: {
      variant: {
        default: "border-input",
        error: "border-destructive focus:ring-destructive",
      },
      size: {
        sm: "h-9 px-3 py-2 text-xs",
        md: "h-10 px-3 py-2 text-sm",
        lg: "h-11 px-4 py-2 text-base",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "md",
    },
  }
)
 
export interface PostObjectItem {
  id: string | number
  title: string
  type?: string
  status?: "publish" | "draft" | "private"
  taxonomy?: string[]
  featured_image?: string
  excerpt?: string
  date?: string
  url?: string
}
 
export interface PostObjectFilter {
  search?: string
  post_type?: string[]
  post_status?: string[]
  taxonomy?: string[]
}
 
export interface PostObjectProps
  extends Omit<
      React.HTMLAttributes<HTMLDivElement>,
      "onChange" | "onLoad" | "onSelect" | "onRemove"
    >,
    VariantProps<typeof postObjectVariants> {
  // Data props
  items?: PostObjectItem[]
  selectedItems?: PostObjectItem[]
 
  // Configuration
  multiple?: boolean
  allowNull?: boolean
  required?: boolean
 
  // Async data loading
  onLoad?: (filter: PostObjectFilter) => Promise<PostObjectItem[]>
  loading?: boolean
 
  // Filtering options
  postTypes?: string[]
  postStatuses?: string[]
  taxonomies?: string[]
  showFilters?: boolean | string[]
 
  // Display options
  showPreview?: boolean
  showFeaturedImage?: boolean
  showExcerpt?: boolean
  showDate?: boolean
  sortable?: boolean
 
  // Return format
  returnFormat?: "object" | "id"
 
  // Event handlers
  onChange?: (items: PostObjectItem[] | string[] | number[] | null) => void
  onSelect?: (item: PostObjectItem) => void
  onRemove?: (item: PostObjectItem) => void
 
  // Labels and messages
  placeholder?: string
  searchPlaceholder?: string
  noItemsText?: string
  loadingText?: string
  instructions?: string
 
  // Validation
  error?: string
}
 
const PostObject = React.forwardRef<HTMLDivElement, PostObjectProps>(
  (
    {
      className,
      variant,
      size,
      items = [],
      selectedItems = [],
      multiple = false,
      allowNull = true,
      required = false,
      onLoad,
      loading = false,
      postTypes = [],
      postStatuses = [],
      taxonomies = [],
      showFilters = false,
      showPreview = false,
      showFeaturedImage = false,
      showExcerpt = false,
      showDate = false,
      sortable = false,
      returnFormat = "object",
      onChange,
      onSelect,
      onRemove,
      placeholder = "Select post...",
      searchPlaceholder = "Search posts...",
      noItemsText = "No posts found",
      loadingText = "Loading...",
      instructions,
      error,
      ...props
    },
    ref
  ) => {
    const [open, setOpen] = React.useState(false)
    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<PostObjectItem[]>(items)
    const [internalSelected, setInternalSelected] = React.useState<PostObjectItem[]>(selectedItems || [])
    const [isLoading, setIsLoading] = React.useState(loading)
 
    // Debounced search
    React.useEffect(() => {
      const timeoutId = setTimeout(() => {
        if (onLoad) {
          setIsLoading(true)
          const filter: PostObjectFilter = {
            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: PostObjectItem) => {
        let newSelected: PostObjectItem[]
 
        if (multiple) {
          if (internalSelected.some((selected) => selected.id === item.id)) {
            // Remove if already selected
            newSelected = internalSelected.filter((selected) => selected.id !== item.id)
          } else {
            // Add to selection
            newSelected = [...internalSelected, item]
          }
        } else {
          // Single selection - replace current selection
          newSelected = [item]
          setOpen(false)
        }
 
        setInternalSelected(newSelected)
        onSelect?.(item)
 
        if (onChange) {
          const result = returnFormat === "object" 
            ? newSelected 
            : newSelected.map((item) => item.id) as string[] | number[]
          onChange(result)
        }
      },
      [internalSelected, multiple, returnFormat, onChange, onSelect]
    )
 
    const handleRemoveItem = React.useCallback(
      (item: PostObjectItem) => {
        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 handleClearAll = React.useCallback(() => {
      setInternalSelected([])
      if (onChange) {
        onChange(returnFormat === "object" ? [] : [])
      }
    }, [onChange, returnFormat])
 
    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"))
 
    const getDisplayText = () => {
      if (internalSelected.length === 0) return placeholder
      if (internalSelected.length === 1) return internalSelected[0]?.title || placeholder
      return `${internalSelected.length} posts selected`
    }
 
    return (
      <div ref={ref} className={cn("space-y-2", className)} {...props}>
        {/* Instructions */}
        {instructions && (
          <p className="text-sm text-muted-foreground">{instructions}</p>
        )}
 
        {/* Filters */}
        {isFiltersVisible && postTypes.length > 0 && (
          <div className="space-y-3 p-3 border rounded-md bg-muted/30">
            <div className="flex flex-col gap-3 sm:flex-row">
              {shouldShowPostType && postTypes.length > 0 && (
                <Select value={selectedPostType} onValueChange={setSelectedPostType}>
                  <SelectTrigger className="w-full sm:w-[140px]">
                    <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-[140px]">
                    <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-[140px]">
                    <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 Selection Interface */}
        <Popover open={open} onOpenChange={setOpen}>
          <PopoverTrigger asChild>
            <Button
              variant="outline"
              role="combobox"
              aria-expanded={open}
              className={cn(
                postObjectVariants({ variant, size }),
                "justify-between font-normal",
                internalSelected.length === 0 && "text-muted-foreground"
              )}
            >
              <span className="truncate">{getDisplayText()}</span>
              <ChevronDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
            </Button>
          </PopoverTrigger>
          <PopoverContent className="w-[400px] p-0" align="start">
            <Command>
              {shouldShowSearch && (
                <CommandInput
                  placeholder={searchPlaceholder}
                  value={searchQuery}
                  onValueChange={setSearchQuery}
                />
              )}
              <CommandList>
                <CommandEmpty>
                  {isLoading ? loadingText : noItemsText}
                </CommandEmpty>
                {allowNull && internalSelected.length > 0 && (
                  <CommandGroup>
                    <CommandItem onSelect={handleClearAll} className="text-muted-foreground">
                      <X className="mr-2 h-4 w-4" />
                      Clear selection{multiple && "s"}
                    </CommandItem>
                  </CommandGroup>
                )}
                <CommandGroup>
                  {filteredItems
                    .filter((item): item is PostObjectItem => item != null && item.id !== undefined)
                    .map((item) => {
                      const isSelected = internalSelected.some((selected) => selected.id === item.id)
                      
                      return (
                        <CommandItem
                          key={item.id}
                          onSelect={() => handleSelectItem(item)}
                          className="flex items-center gap-3 p-3"
                        >
                          <div className="flex h-4 w-4 items-center justify-center">
                            {isSelected && <Check className="h-4 w-4" />}
                          </div>
                          
                          {showFeaturedImage && item.featured_image && (
                            <img
                              src={item.featured_image}
                              alt={item.title}
                              className="h-8 w-8 rounded object-cover"
                            />
                          )}
                          
                          <div className="flex-1 min-w-0">
                            <div className="flex items-center gap-2">
                              <span className="font-medium truncate">{item.title}</span>
                              {item.status && item.status !== "publish" && (
                                <Badge variant="secondary" className="text-xs">
                                  {item.status}
                                </Badge>
                              )}
                              {item.type && (
                                <Badge variant="outline" className="text-xs">
                                  {item.type}
                                </Badge>
                              )}
                            </div>
                            
                            {showExcerpt && item.excerpt && (
                              <p className="text-xs text-muted-foreground mt-1 line-clamp-2">
                                {item.excerpt}
                              </p>
                            )}
                            
                            {showDate && item.date && (
                              <span className="text-xs text-muted-foreground">
                                {item.date}
                              </span>
                            )}
                          </div>
                          
                          {showPreview && item.url && (
                            <Button
                              variant="ghost"
                              size="sm"
                              className="h-8 w-8 p-0"
                              asChild
                              onClick={(e) => e.stopPropagation()}
                            >
                              <a href={item.url} target="_blank" rel="noopener noreferrer">
                                <Eye className="h-4 w-4" />
                              </a>
                            </Button>
                          )}
                        </CommandItem>
                      )
                    })}
                </CommandGroup>
              </CommandList>
            </Command>
          </PopoverContent>
        </Popover>
 
        {/* Selected Items (for multiple selection) */}
        {multiple && internalSelected.length > 0 && (
          <div className="space-y-2 p-3 border rounded-md bg-muted/30">
            <h4 className="text-sm font-medium">Selected Posts</h4>
            <div className="space-y-1">
              {internalSelected.map((item, index) => (
                <div
                  key={item.id}
                  className="flex items-center gap-2 p-2 bg-background rounded border"
                >
                  {sortable && (
                    <GripVertical className="h-4 w-4 text-muted-foreground cursor-move" />
                  )}
                  
                  {showFeaturedImage && item.featured_image && (
                    <img
                      src={item.featured_image}
                      alt={item.title}
                      className="h-6 w-6 rounded object-cover"
                    />
                  )}
                  
                  <div className="flex-1 min-w-0">
                    <span className="text-sm font-medium truncate">{item.title}</span>
                    {item.type && (
                      <Badge variant="outline" className="ml-2 text-xs">
                        {item.type}
                      </Badge>
                    )}
                  </div>
                  
                  <Button
                    variant="ghost"
                    size="sm"
                    className="h-6 w-6 p-0"
                    onClick={() => handleRemoveItem(item)}
                  >
                    <X className="h-3 w-3" />
                  </Button>
                </div>
              ))}
            </div>
          </div>
        )}
 
        {/* Error Message */}
        {error && (
          <p className="text-sm text-destructive">{error}</p>
        )}
      </div>
    )
  }
)
PostObject.displayName = "PostObject"
 
export { PostObject, postObjectVariants } 

Update the import paths to match your project setup.

Usage

import { PostObject } from "@/components/ui/post-object"
<PostObject
  items={posts}
  selectedItems={selectedPosts}
  onChange={(items) => setSelectedPosts(items)}
  placeholder="Select posts..."
  allowNull
/>

Examples

Basic

A basic post object field with static data and search functionality.

Featured Post

Select a post to feature on the homepage

"use client"
 
import { useState } from "react"
import { PostObject, type PostObjectItem } from "@/components/ui/post-object"
 
const posts = [
  {
    id: 1,
    title: "Getting Started with React",
    type: "post",
    status: "publish" as const,
    excerpt: "Learn the fundamentals of React development with this comprehensive guide.",
    date: "2024-01-15",
  },
  {
    id: 2,
    title: "Advanced TypeScript Patterns",
    type: "post", 
    status: "publish" as const,
    excerpt: "Explore advanced TypeScript patterns for better type safety.",
    date: "2024-01-10",
  },
  {
    id: 3,
    title: "Building UIs with Tailwind CSS",
    type: "post",
    status: "draft" as const,
    excerpt: "Create beautiful, responsive interfaces with Tailwind CSS.",
    date: "2024-01-05",
  },
  {
    id: 4,
    title: "State Management Guide",
    type: "guide",
    status: "publish" as const,
    excerpt: "Complete guide to managing state in modern React applications.",
    date: "2024-01-20",
  },
  {
    id: 5,
    title: "API Design Best Practices",
    type: "guide",
    status: "publish" as const,
    excerpt: "Design robust APIs that scale with your application needs.",
    date: "2024-01-12",
  },
] satisfies PostObjectItem[]
 
export function PostObjectDemo() {
  const [selectedPost, setSelectedPost] = useState<PostObjectItem | null>(null)
 
  return (
    <div className="w-full max-w-2xl space-y-4">
      <div>
        <h3 className="text-lg font-semibold">Featured Post</h3>
        <p className="text-sm text-muted-foreground">
          Select a post to feature on the homepage
        </p>
      </div>
 
      <PostObject
        items={posts}
        selectedItems={selectedPost ? [selectedPost] : []}
        onChange={(items) => {
          const postItems = items as PostObjectItem[]
          setSelectedPost(postItems.length > 0 ? postItems[0] || null : null)
        }}
        multiple={false}
        allowNull={true}
        postTypes={[]}
        postStatuses={["publish", "draft"]}
        showFilters={["search", "post_type"]}
        placeholder="Select a featured post..."
        searchPlaceholder="Search posts..."
        showExcerpt={true}
        showDate={true}
      />
 
      {selectedPost && (
        <div className="mt-6 p-4 bg-muted/30 rounded-lg">
          <h4 className="font-medium">Selected Post:</h4>
          <div className="mt-2">
            <div className="font-medium">{selectedPost.title}</div>
            <div className="text-sm text-muted-foreground mt-1">
              {selectedPost.excerpt}
            </div>
            <div className="flex items-center gap-2 mt-2 text-xs text-muted-foreground">
              <span className="capitalize">{selectedPost.type}</span>
              <span>•</span>
              <span>{selectedPost.date}</span>
              <span>•</span>
              <span className="capitalize">{selectedPost.status}</span>
            </div>
          </div>
        </div>
      )}
    </div>
  )
} 

Multiple Selection

Configure for multiple post selection with filters and sortable display.

Related Content

Select up to 5 related posts to display in the sidebar

Choose posts that are related to the current content. You can select up to 5 posts.

"use client"
 
import { useState } from "react"
import { PostObject, type PostObjectItem } from "@/components/ui/post-object"
 
const posts = [
  {
    id: 1,
    title: "React Performance Optimization",
    type: "post",
    status: "publish" as const,
    taxonomy: ["React", "Performance"],
    excerpt: "Learn advanced techniques to optimize React applications for better performance.",
    date: "2024-01-15",
    featured_image: "https://picsum.photos/400/300?random=1",
  },
  {
    id: 2,
    title: "TypeScript Best Practices",
    type: "post",
    status: "publish" as const,
    taxonomy: ["TypeScript", "Best Practices"],
    excerpt: "Comprehensive guide to TypeScript best practices for enterprise applications.",
    date: "2024-01-10",
    featured_image: "https://picsum.photos/400/300?random=2",
  },
  {
    id: 3,
    title: "CSS Grid Mastery",
    type: "tutorial",
    status: "publish" as const,
    taxonomy: ["CSS", "Grid"],
    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",
  },
  {
    id: 4,
    title: "Next.js 14 Features",
    type: "guide",
    status: "publish" as const,
    taxonomy: ["Next.js", "React"],
    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",
  },
  {
    id: 5,
    title: "Database Optimization",
    type: "guide",
    status: "draft" as const,
    taxonomy: ["Database", "Performance"],
    excerpt: "Optimize database queries and improve application performance.",
    date: "2024-01-12",
    featured_image: "https://picsum.photos/400/300?random=5",
  },
  {
    id: 6,
    title: "GraphQL API Design",
    type: "post",
    status: "publish" as const,
    taxonomy: ["GraphQL", "API"],
    excerpt: "Design efficient GraphQL APIs with proper schema design and resolver patterns.",
    date: "2024-01-25",
    featured_image: "https://picsum.photos/400/300?random=6",
  },
  {
    id: 7,
    title: "Testing Strategies",
    type: "guide",
    status: "publish" as const,
    taxonomy: ["Testing", "QA"],
    excerpt: "Comprehensive testing strategies for modern web applications.",
    date: "2024-01-18",
    featured_image: "https://picsum.photos/400/300?random=7",
  },
  {
    id: 8,
    title: "DevOps Fundamentals",
    type: "tutorial",
    status: "private" as const,
    taxonomy: ["DevOps", "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=8",
  },
] satisfies PostObjectItem[]
 
export function PostObjectMultipleDemo() {
  const [selectedPosts, setSelectedPosts] = useState<PostObjectItem[]>([])
 
  return (
    <div className="w-full max-w-4xl space-y-4">
      <div>
        <h3 className="text-lg font-semibold">Related Content</h3>
        <p className="text-sm text-muted-foreground">
          Select up to 5 related posts to display in the sidebar
        </p>
      </div>
 
      <PostObject
        items={posts}
        selectedItems={selectedPosts}
        onChange={(items) => setSelectedPosts(items as PostObjectItem[])}
        multiple={true}
        postTypes={["post", "guide", "tutorial"]}
        postStatuses={["publish", "draft", "private"]}
        taxonomies={["React", "TypeScript", "CSS", "Next.js", "Database", "GraphQL", "Testing", "DevOps"]}
        showFilters={["search", "post_type", "post_status", "taxonomy"]}
        placeholder="Select related posts..."
        searchPlaceholder="Search by title or content..."
        showExcerpt={true}
        showDate={true}
        showFeaturedImage={true}
        instructions="Choose posts that are related to the current content. You can select up to 5 posts."
      />
 
      {selectedPosts.length > 0 && (
        <div className="mt-6 p-4 bg-muted/30 rounded-lg">
          <h4 className="font-medium mb-3">Selected Posts ({selectedPosts.length}):</h4>
          <div className="grid gap-3">
            {selectedPosts.map((post) => (
              <div key={post.id} className="flex items-center gap-3 p-3 bg-background rounded border">
                {post.featured_image && (
                  <img
                    src={post.featured_image}
                    alt={post.title}
                    className="w-16 h-16 rounded object-cover flex-shrink-0"
                  />
                )}
                <div className="flex-1 min-w-0">
                  <div className="font-medium text-sm">{post.title}</div>
                  <div className="text-xs text-muted-foreground mt-1">
                    {post.excerpt}
                  </div>
                  <div className="flex items-center gap-2 mt-2">
                    <span className="text-xs bg-primary/10 text-primary px-2 py-1 rounded">
                      {post.type}
                    </span>
                    <span className="text-xs bg-secondary/10 text-secondary-foreground px-2 py-1 rounded">
                      {post.status}
                    </span>
                    <span className="text-xs text-muted-foreground">
                      {post.date}
                    </span>
                  </div>
                  {post.taxonomy && (
                    <div className="flex flex-wrap gap-1 mt-2">
                      {post.taxonomy.map((tag) => (
                        <span key={tag} className="text-xs bg-muted text-muted-foreground px-2 py-1 rounded">
                          {tag}
                        </span>
                      ))}
                    </div>
                  )}
                </div>
              </div>
            ))}
          </div>
        </div>
      )}
    </div>
  )
} 

Async Loading

Load posts dynamically with search and filtering. Perfect for large datasets.

Dynamic 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 { PostObject, type PostObjectItem, type PostObjectFilter } from "@/components/ui/post-object"
 
// Simulate a large dataset
const mockDatabase = [
  {
    id: 1,
    title: "Introduction to React Hooks",
    type: "post",
    status: "publish" as const,
    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" as const,
    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" as const,
    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" as const,
    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" as const,
    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" as const,
    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" as const,
    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" as const,
    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" as const,
    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" as const,
    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",
  },
] satisfies PostObjectItem[]
 
// Simulate API call with delay
const fetchPosts = async (filter: PostObjectFilter): Promise<PostObjectItem[]> => {
  // 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 PostObjectAsyncDemo() {
  const [selectedPosts, setSelectedPosts] = useState<PostObjectItem[]>([])
 
  return (
    <div className="w-full max-w-4xl mx-auto space-y-4">
      <div>
        <h3 className="text-lg font-semibold mb-2">Dynamic Content Loader</h3>
        <p className="text-sm text-muted-foreground mb-4">
          Search and filter from a large dataset with async loading
        </p>
      </div>
 
      <PostObject
        onLoad={fetchPosts}
        selectedItems={selectedPosts}
        onChange={(items) => setSelectedPosts(items as PostObjectItem[])}
        multiple={true}
        postTypes={["post", "guide", "tutorial"]}
        postStatuses={["publish", "draft"]}
        taxonomies={["React", "TypeScript", "CSS", "API", "Testing", "Performance"]}
        showFilters={["search", "post_type", "post_status", "taxonomy"]}
        showFeaturedImage={true}
        showExcerpt={true}
        showDate={true}
        showPreview={true}
        instructions="Start typing to search through our content database"
        placeholder="Search posts..."
        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>
  )
} 

Data Structure

PostObjectItem

The component expects items to follow this structure:

interface PostObjectItem {
  id: string | number
  title: string
  type?: string
  status?: "publish" | "draft" | "private"
  taxonomy?: string[]
  featured_image?: string
  excerpt?: string
  date?: string
  url?: string
}

Example Data

const posts: PostObjectItem[] = [
  {
    id: 1,
    title: "Getting Started with React",
    type: "post",
    status: "publish",
    taxonomy: ["React", "JavaScript"],
    excerpt: "Learn the basics of React development...",
    date: "2024-01-15",
    url: "/posts/getting-started-react"
  },
  // ... more items
]

Async Loading

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

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

<PostObject
  onLoad={fetchPosts}
  selectedItems={selected}
  onChange={setSelected}
  postTypes={["post", "page"]}
  postStatuses={["publish", "draft"]}
  showFilters={["search", "post_type"]}
/>

Filtering Options

Control which filters are displayed using the showFilters prop:

// Show all filters
<PostObject showFilters={true} />

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

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

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

Return Formats

Choose how selected items are returned:

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

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

Configuration Options

Single vs Multiple Selection

// Single selection (default)
<PostObject
  multiple={false}
  placeholder="Select a post..."
  onChange={(items) => {
    const post = items[0] // Single item
  }}
/>

// Multiple selection
<PostObject
  multiple={true}
  placeholder="Select posts..."
  sortable={true} // Enable drag-and-drop reordering
  onChange={(items) => {
    // Array of items
  }}
/>

Allow Null Option

<PostObject
  allowNull={true}  // Show "Clear selection" option
  required={false}  // Not required
  placeholder="Select a post (optional)..."
/>

API Reference

PostObject

PropTypeDefaultDescription
itemsPostObjectItem[][]Static items to display
selectedItemsPostObjectItem[][]Currently selected items
onChange(items: PostObjectItem[] | string[] | number[] | null) => void-Callback when selection changes
onLoad(filter: PostObjectFilter) => Promise<PostObjectItem[]>-Async data loading function
multiplebooleanfalseAllow multiple selections
allowNullbooleantrueAllow clearing selections
requiredbooleanfalseWhether field is required
postTypesstring[][]Available post types for filtering
postStatusesstring[][]Available post statuses for filtering
taxonomiesstring[][]Available taxonomies for filtering
showFiltersboolean | string[]falseWhich filters to show
showPreviewbooleanfalseShow preview links
showFeaturedImagebooleanfalseShow featured images
showExcerptbooleanfalseShow item excerpts
showDatebooleanfalseShow item dates
sortablebooleanfalseEnable drag-and-drop sorting
returnFormat"object" | "id""object"Format of returned data
placeholderstring"Select post..."Dropdown placeholder text
searchPlaceholderstring"Search posts..."Search input placeholder
noItemsTextstring"No posts found"Text when no items available
loadingTextstring"Loading..."Loading state text
instructionsstring-Help text above the field
errorstring-Error message to display
variant"default" | "error""default"Visual variant
size"sm" | "md" | "lg""md"Size variant

Event Handlers

PropTypeDescription
onSelect(item: PostObjectItem) => voidCalled when an item is selected
onRemove(item: PostObjectItem) => voidCalled when an item is removed

PostObjectItem Interface

interface PostObjectItem {
  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
}

PostObjectFilter Interface

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

Common Patterns

Content Management System

const [parentPage, setParentPage] = useState<PostObjectItem | null>(null)

<PostObject
  items={pages}
  selectedItems={parentPage ? [parentPage] : []}
  onChange={(items) => {
    const pageItems = items as PostObjectItem[]
    setParentPage(pageItems[0] || null)
  }}
  multiple={false}
  placeholder="Select parent page..."
  showFilters={["search"]}
  postTypes={["page"]}
  allowNull
/>
const [relatedPosts, setRelatedPosts] = useState<PostObjectItem[]>([])

<PostObject
  onLoad={fetchPosts}
  selectedItems={relatedPosts}
  onChange={(items) => setRelatedPosts(items as PostObjectItem[])}
  multiple={true}
  placeholder="Select related posts..."
  showFilters={["search", "post_type"]}
  postTypes={["post", "tutorial", "guide"]}
  sortable={true}
  returnFormat="id"
/>
const [featuredContent, setFeaturedContent] = useState<PostObjectItem | null>(null)

<PostObject
  items={content}
  selectedItems={featuredContent ? [featuredContent] : []}
  onChange={(items) => {
    const contentItems = items as PostObjectItem[]
    setFeaturedContent(contentItems[0] || null)
  }}
  placeholder="Select featured content..."
  showFilters={["search", "post_type", "post_status"]}
  postTypes={["post", "page", "product"]}
  postStatuses={["publish"]}
  showExcerpt={true}
  showFeaturedImage={true}
  allowNull
/>

Accessibility

The Post Object component follows accessibility best practices:

  • Keyboard Navigation - Full keyboard support with arrow keys and Enter
  • Screen Reader Support - Proper ARIA labels and live regions
  • Focus Management - Clear focus indicators and logical tab order
  • High Contrast - Supports high contrast mode
  • Combobox Pattern - Follows WAI-ARIA combobox guidelines

Comparison with Relationship Field

FeaturePost ObjectRelationship
InterfaceDropdown/SelectDual-panel
Best forSingle or few selectionsMultiple selections
Screen spaceCompactMore spacious
Allow Null✅ Yes❌ No
SearchIn dropdownSeparate input
FilteringOptional filtersAlways visible
Use caseQuick selectionRelationship management