
3D Extruded Buildings & Urban Footprints
Real-world 3D building extrusions with dynamic sunlight shadows, terrain DEM elevations, and pitch/bearing camera controls.
Centered marketing team section. Eyebrow + headline + lede over a responsive member card grid (initials avatar, role badge, one-line bio, ghost social buttons). Members with a department field group under uppercase headers; variant="compact" drops bios for tighter cards. Roster is stub data — edit DEFAULT_MEMBERS or pass members.
Also available for Vue ->$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/team-section.json$npx shadcn@latest add https://uipkge.dev/r/react/team-section.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/team-section.json$bunx shadcn@latest add https://uipkge.dev/r/react/team-section.jsonnpx shadcn@latest add @uipkge-react/team-sectionInstalls to:components/blocks/| Name | Type / Values | Default | Required |
|---|---|---|---|
eyebrow | string | — | optional |
title | string | — | optional |
description | string | — | optional |
members | TeamMember[] | — | optional |
variant'grid' shows full cards with bios; 'compact' tightens cards and drops them. | 'grid' | 'compact' | — | optional |
className | string | — | optional |
Type aliases exported from this item's source. Use these to shape the data you pass in.
TeamMemberinterface TeamMember {
name: string
role: string
bio?: string
/** When any member has a department, the grid groups under uppercase headers. */
department?: string
twitter?: string
github?: string
linkedin?: string
}MemberSectioninterface MemberSection {
department?: string
members: TeamMember[]
}'use client'
import { Avatar, AvatarFallback } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { Card, CardContent } from '@/components/ui/card'
import { cn } from '@/lib/utils'
export interface TeamMember {
name: string
role: string
bio?: string
/** When any member has a department, the grid groups under uppercase headers. */
department?: string
twitter?: string
github?: string
linkedin?: string
}
/** Stub roster — swap for your data source after installing. */
const DEFAULT_MEMBERS: TeamMember[] = [
{
name: 'Amara Okafor',
role: 'Co-founder & CEO',
bio: 'Sets the vision and still reviews every release note before it ships.',
twitter: 'https://x.com/amaraokafor',
linkedin: 'https://linkedin.com/in/amaraokafor',
},
{
name: 'Jonas Lindqvist',
role: 'Co-founder & CTO',
bio: 'Owns architecture and keeps p95 latency under 80ms in every region.',
github: 'https://github.com/jonaslindqvist',
linkedin: 'https://linkedin.com/in/jonaslindqvist',
},
{
name: 'Priya Raghavan',
role: 'Staff Engineer',
bio: 'Leads the platform team and maintains our public REST and GraphQL APIs.',
github: 'https://github.com/priyaraghavan',
},
{
name: 'Marco Deluca',
role: 'Product Designer',
bio: 'Turns messy workflows into calm interfaces and runs our weekly design critique.',
twitter: 'https://x.com/marcodeluca',
github: 'https://github.com/marcodeluca',
linkedin: 'https://linkedin.com/in/marcodeluca',
},
{
name: 'Tomoko Saito',
role: 'Head of People',
bio: 'Builds hiring loops that respect candidates and onboarding that actually sticks.',
linkedin: 'https://linkedin.com/in/tomokosaito',
},
{
name: 'Felix Adeyemi',
role: 'Growth Engineer',
bio: 'Experiments across onboarding and pricing pages; ships behind flags daily.',
twitter: 'https://x.com/felixadeyemi',
},
]
export interface TeamSectionProps {
eyebrow?: string
title?: string
description?: string
members?: TeamMember[]
/** 'grid' shows full cards with bios; 'compact' tightens cards and drops them. */
variant?: 'grid' | 'compact'
className?: string
}
interface MemberSection {
department?: string
members: TeamMember[]
}
function initialsFor(name: string): string {
return name
.split(/\s+/)
.filter(Boolean)
.slice(0, 2)
.map((part) => part.charAt(0).toUpperCase())
.join('')
}
function getSections(members: TeamMember[]): MemberSection[] {
if (!members.some((member) => member.department)) return [{ members }]
const grouped = new Map<string, TeamMember[]>()
for (const member of members) {
const key = member.department ?? ''
if (!grouped.has(key)) grouped.set(key, [])
grouped.get(key)!.push(member)
}
return Array.from(grouped.entries()).map(([department, groupedMembers]) => ({
department: department || undefined,
members: groupedMembers,
}))
}
function XIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
</svg>
)
}
function GithubIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.39 7.86 10.91.58.11.79-.25.79-.55v-2.15c-3.2.69-3.87-1.36-3.87-1.36-.52-1.33-1.28-1.68-1.28-1.68-1.04-.71.08-.7.08-.7 1.16.08 1.77 1.19 1.77 1.19 1.02 1.76 2.69 1.25 3.35.96.1-.75.4-1.25.72-1.54-2.55-.29-5.24-1.28-5.24-5.68 0-1.26.45-2.28 1.19-3.09-.12-.29-.52-1.46.11-3.05 0 0 .97-.31 3.18 1.18a11.1 11.1 0 0 1 5.78 0c2.21-1.49 3.18-1.18 3.18-1.18.63 1.59.23 2.76.11 3.05.74.81 1.19 1.83 1.19 3.09 0 4.41-2.69 5.38-5.26 5.66.41.35.78 1.05.78 2.12v3.14c0 .31.21.67.8.55A11.51 11.51 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5z" />
</svg>
)
}
function LinkedinIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" {...props}>
<path d="M20.45 20.45h-3.55v-5.57c0-1.33-.03-3.04-1.85-3.04-1.85 0-2.14 1.45-2.14 2.94v5.67H9.36V9h3.41v1.56h.05c.47-.9 1.63-1.85 3.36-1.85 3.6 0 4.27 2.37 4.27 5.46zM5.34 7.43a2.06 2.06 0 1 1 0-4.12 2.06 2.06 0 0 1 0 4.12zM7.12 20.45H3.56V9h3.56zM22.22 0H1.77C.79 0 0 .77 0 1.72v20.55C0 23.23.79 24 1.77 24h20.45c.98 0 1.78-.77 1.78-1.73V1.72C24 .77 23.2 0 22.22 0z" />
</svg>
)
}
export function TeamSection({
eyebrow = 'Our team',
title = 'The people behind the product',
description = 'A small team with strong opinions about craft. We design, build and support everything ourselves — no outsourcing, no black boxes.',
members = DEFAULT_MEMBERS,
variant = 'grid',
className,
}: TeamSectionProps) {
const compact = variant === 'compact'
const showHeader = Boolean(eyebrow || title || description)
const sections = getSections(members)
return (
<section data-slot="team-section" className={cn('bg-background', className)}>
<div className="mx-auto max-w-6xl px-6 py-24">
{showHeader && (
<div data-slot="team-section-header" className="mx-auto max-w-2xl text-center">
{eyebrow && <p className="text-primary text-sm font-medium tracking-widest uppercase">{eyebrow}</p>}
{title && <h2 className="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">{title}</h2>}
{description && <p className="text-muted-foreground mt-3 text-lg leading-relaxed">{description}</p>}
</div>
)}
{sections.map((section, i) => (
<div
key={section.department ?? 'team'}
data-slot="team-section-group"
className={i > 0 || showHeader ? 'mt-12' : undefined}
>
{section.department && (
<h3 className="text-muted-foreground text-xs font-semibold tracking-widest uppercase">
{section.department}
</h3>
)}
<div
data-slot="team-section-grid"
className={cn(
'grid gap-6 sm:grid-cols-2 lg:grid-cols-3',
compact && 'gap-4',
section.department && 'mt-6',
)}
>
{section.members.map((member) => (
<Card key={member.name} className="text-center">
<CardContent className={cn('flex flex-col items-center', compact ? 'gap-3 p-4' : 'gap-4 p-6')}>
<Avatar className={compact ? 'size-10' : 'size-16'}>
<AvatarFallback className={compact ? 'text-sm' : 'text-lg'}>
{initialsFor(member.name)}
</AvatarFallback>
</Avatar>
<div className="space-y-1">
<p className="text-sm font-semibold">{member.name}</p>
<Badge variant="secondary">{member.role}</Badge>
{!compact && member.bio && (
<p className="text-muted-foreground line-clamp-2 text-xs leading-relaxed">{member.bio}</p>
)}
</div>
{(member.twitter || member.github || member.linkedin) && (
<div className="flex items-center gap-0.5">
{member.twitter && (
<Button variant="ghost" size="icon-sm" asChild>
<a
href={member.twitter}
target="_blank"
rel="noreferrer"
aria-label={`${member.name} on X (Twitter)`}
>
<XIcon className="size-4" />
</a>
</Button>
)}
{member.github && (
<Button variant="ghost" size="icon-sm" asChild>
<a
href={member.github}
target="_blank"
rel="noreferrer"
aria-label={`${member.name} on GitHub`}
>
<GithubIcon className="size-4" />
</a>
</Button>
)}
{member.linkedin && (
<Button variant="ghost" size="icon-sm" asChild>
<a
href={member.linkedin}
target="_blank"
rel="noreferrer"
aria-label={`${member.name} on LinkedIn`}
>
<LinkedinIcon className="size-4" />
</a>
</Button>
)}
</div>
)}
</CardContent>
</Card>
))}
</div>
</div>
))}
</div>
</section>
)
}
Raw manifest:https://uipkge.dev/r/react/team-section.json