
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Full search results page: large pre-filled search input with result count, removable filter chips with clear-all, a left facet sidebar with Type and Team checkbox groups (hidden below lg), six result rows with icon tiles, titles, paths, snippets, and type/updated meta, plus a Previous/Next pagination footer. Chips and facets are interactive; swap the stub rows for your search source.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/search-results-page.json$npx shadcn@latest add https://uipkge.dev/r/react/search-results-page.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/search-results-page.json$bunx shadcn@latest add https://uipkge.dev/r/react/search-results-page.jsonnpx shadcn@latest add @uipkge-react/search-results-pageInstalls to:components/blocks/Type aliases exported from this item's source. Use these to shape the data you pass in.
SearchResultinterface SearchResult {
id: string
icon: React.ComponentType<{ className?: string }>
title: string
path: string
snippet: string
type: string
updated: string
}FacetOptioninterface FacetOption {
id: string
label: string
count: number
}FacetGroupinterface FacetGroup {
id: string
label: string
options: FacetOption[]
}'use client'
import * as React from 'react'
import { BookOpen, FileText, GitBranch, MessageSquare, Search, Users, Video } from 'lucide-react'
import { cn } from '@/lib/utils'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Checkbox } from '@/components/ui/checkbox'
import { Chip } from '@/components/ui/chip'
import { Input } from '@/components/ui/input'
interface SearchResult {
id: string
icon: React.ComponentType<{ className?: string }>
title: string
path: string
snippet: string
type: string
updated: string
}
interface FacetOption {
id: string
label: string
count: number
}
interface FacetGroup {
id: string
label: string
options: FacetOption[]
}
export interface SearchResultsPageProps extends React.HTMLAttributes<HTMLDivElement> {}
const RESULTS: SearchResult[] = [
{
id: 'r1',
icon: BookOpen,
title: 'Onboarding flows: design your first-run experience',
path: 'docs.acme.dev / guides / onboarding-flows',
snippet:
'Learn how to compose onboarding flows from checklists, tours, and progressive disclosure so new users reach their first aha moment faster.',
type: 'Docs',
updated: '2 days ago',
},
{
id: 'r2',
icon: FileText,
title: 'Checklist API reference for onboarding flows',
path: 'docs.acme.dev / api / checklist',
snippet:
'Full reference for the Checklist primitive used to drive onboarding flows, including step completion events, persistence, and theming.',
type: 'Reference',
updated: '5 hours ago',
},
{
id: 'r3',
icon: Video,
title: 'Video walkthrough: building onboarding flows in 20 minutes',
path: 'learn.acme.dev / videos / onboarding-flows-walkthrough',
snippet:
'A hands-on screencast that builds a complete onboarding flow with branching steps, skippable tours, and analytics instrumentation.',
type: 'Video',
updated: '1 week ago',
},
{
id: 'r4',
icon: MessageSquare,
title: 'How do you measure completion of onboarding flows?',
path: 'community.acme.dev / discussions / 4821',
snippet:
'Community thread comparing activation-rate funnels, step-drop-off charts, and qualitative surveys for measuring onboarding flow success.',
type: 'Discussion',
updated: '3 days ago',
},
{
id: 'r5',
icon: GitBranch,
title: 'Example repo: onboarding flows with A/B-tested variants',
path: 'github.com / acme / examples / onboarding-ab-test',
snippet:
'Runnable example that splits new signups into two onboarding flow variants and reports which one converts better to first project creation.',
type: 'Example',
updated: '2 weeks ago',
},
{
id: 'r6',
icon: Users,
title: 'Playbook: personalizing onboarding flows by team role',
path: 'docs.acme.dev / playbooks / role-based-onboarding',
snippet:
'Patterns for tailoring onboarding flows to admins, editors, and viewers, with sample copy, suggested defaults, and rollout checklists.',
type: 'Guide',
updated: '4 days ago',
},
]
const FACET_GROUPS: FacetGroup[] = [
{
id: 'type',
label: 'Type',
options: [
{ id: 'type-docs', label: 'Docs', count: 18 },
{ id: 'type-guides', label: 'Guides', count: 11 },
{ id: 'type-video', label: 'Video', count: 7 },
{ id: 'type-discussion', label: 'Discussion', count: 6 },
],
},
{
id: 'team',
label: 'Team',
options: [
{ id: 'team-product', label: 'Product', count: 14 },
{ id: 'team-engineering', label: 'Engineering', count: 16 },
{ id: 'team-design', label: 'Design', count: 8 },
{ id: 'team-support', label: 'Support', count: 4 },
],
},
]
const TOTAL_PAGES = 3
export const SearchResultsPage = React.forwardRef<HTMLDivElement, SearchResultsPageProps>(
({ className, ...props }, ref) => {
const [query, setQuery] = React.useState('onboarding flows')
const [chips, setChips] = React.useState([
{ id: 'type-docs', label: 'Type: Docs' },
{ id: 'team-product', label: 'Team: Product' },
])
const [activeFacets, setActiveFacets] = React.useState<Set<string>>(() => new Set(['type-docs', 'team-product']))
const [page, setPage] = React.useState(1)
function removeChip(id: string) {
setChips((prev) => prev.filter((chip) => chip.id !== id))
}
function clearAll() {
setChips([])
}
function toggleFacet(id: string, checked: boolean | 'indeterminate') {
setActiveFacets((prev) => {
const next = new Set(prev)
if (checked === true) next.add(id)
else next.delete(id)
return next
})
}
const start = (page - 1) * RESULTS.length + 1
const resultRange = `${start}-${start + RESULTS.length - 1}`
return (
<div ref={ref} data-slot="search-results-page" className={cn('w-full space-y-6', className)} {...props}>
<div className="space-y-3">
<div className="relative max-w-xl">
<Search
aria-hidden="true"
className="text-muted-foreground pointer-events-none absolute top-1/2 left-3 size-4 -translate-y-1/2"
/>
<Input
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
className="h-11 pl-9 text-base"
/>
</div>
<p className="text-muted-foreground text-sm">
{resultRange} of 42 results for “{query}”
</p>
</div>
{chips.length > 0 && (
<div className="flex flex-wrap items-center gap-2">
{chips.map((chip) => (
<Chip key={chip.id} variant="outline" size="sm" closable onClose={() => removeChip(chip.id)}>
{chip.label}
</Chip>
))}
<Button variant="link" size="sm" className="text-muted-foreground h-auto min-h-6 px-2" onClick={clearAll}>
Clear all
</Button>
</div>
)}
<div className="flex gap-8">
<aside className="hidden w-48 shrink-0 space-y-6 lg:block" aria-label="Filters">
{FACET_GROUPS.map((group) => (
<div key={group.id}>
<h3 className="text-muted-foreground mb-3 text-xs font-medium tracking-wider uppercase">
{group.label}
</h3>
<ul className="space-y-2.5">
{group.options.map((option) => (
<li key={option.id}>
<label
htmlFor={`${group.id}-${option.id}`}
className="flex cursor-pointer items-center gap-2 text-sm"
>
<Checkbox
id={`${group.id}-${option.id}`}
checked={activeFacets.has(option.id)}
onCheckedChange={(checked) => toggleFacet(option.id, checked)}
/>
<span>{option.label}</span>
<span className="text-muted-foreground ml-auto text-xs tabular-nums">{option.count}</span>
</label>
</li>
))}
</ul>
</div>
))}
</aside>
<div className="min-w-0 flex-1">
<ol className="divide-border divide-y">
{RESULTS.map((result) => (
<li key={result.id}>
<a
href="#"
className="hover:bg-muted/50 focus-visible:ring-ring block rounded-lg p-4 transition-colors focus-visible:ring-2 focus-visible:outline-none"
>
<div className="flex gap-4">
<div className="bg-muted/50 text-muted-foreground flex size-10 shrink-0 items-center justify-center rounded-md border">
<result.icon aria-hidden="true" className="size-5" />
</div>
<div className="min-w-0 space-y-1">
<p className="truncate font-medium">{result.title}</p>
<p className="text-muted-foreground truncate text-xs">{result.path}</p>
<p className="text-muted-foreground line-clamp-2 text-sm">{result.snippet}</p>
<div className="flex items-center gap-3 pt-1">
<Badge variant="secondary">{result.type}</Badge>
<span className="text-muted-foreground text-xs">Updated {result.updated}</span>
</div>
</div>
</div>
</a>
</li>
))}
</ol>
<nav className="flex items-center justify-between pt-4" aria-label="Pagination">
<Button variant="outline" size="sm" disabled={page === 1} onClick={() => setPage(page - 1)}>
Previous
</Button>
<div className="flex items-center gap-1">
{Array.from({ length: TOTAL_PAGES }, (_, i) => i + 1).map((n) => (
<Button
key={n}
variant={n === page ? 'outline' : 'ghost'}
size="icon-sm"
aria-current={n === page ? 'page' : undefined}
onClick={() => setPage(n)}
>
{n}
</Button>
))}
</div>
<Button variant="outline" size="sm" disabled={page === TOTAL_PAGES} onClick={() => setPage(page + 1)}>
Next
</Button>
</nav>
</div>
</div>
</div>
)
},
)
SearchResultsPage.displayName = 'SearchResultsPage'
Raw manifest:https://uipkge.dev/r/react/search-results-page.json