Team with open roles
Who we are and who we are looking for, in one section.
Live preview
Source
This exact file renders the preview above.
import { ArrowRight, MapPin } from 'lucide-react'
import Link from 'next/link'
import { Container } from '@/components/ui/layout'
import { AvatarGroup } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { team } from '@/content/demo'
/**
* Team section with open roles
*
* Combines "who we are" with "we are hiring", because a careers section
* detached from the team it would join reads as a job board. Each role links
* to the application flow that actually exists in this demo.
*/
const roles = [
{ title: 'Frontend engineer, design systems', location: 'Remote — GMT ± 3', type: 'Full time' },
{ title: 'Accessibility specialist', location: 'Remote — Europe', type: 'Full time' },
{ title: 'Documentation engineer', location: 'Remote — worldwide', type: 'Contract' },
]
export default function HiringTeam() {
return (
<section className="border-b border-line bg-canvas py-section">
<Container>
<div className="grid gap-10 lg:grid-cols-[1fr_1.3fr] lg:gap-16">
<div>
<h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
Eight people, three open roles.
</h2>
<p className="mt-4 text-md text-ink-muted">
Small enough that everyone touches the component set, large enough that nobody is the
only person who understands a part of it.
</p>
<AvatarGroup
names={team.map((member) => member.name)}
size="md"
max={6}
label="The Foundry team"
className="mt-6"
/>
</div>
<ul className="divide-y divide-[var(--color-border-subtle)] rounded-xl border border-line bg-surface">
{roles.map((role) => (
<li key={role.title}>
<Link
href="/forms/application"
className="group flex items-center gap-4 p-5 transition-colors hover:bg-surface-sunken"
>
<span className="min-w-0 flex-1">
<span className="block text-sm font-semibold text-ink-strong group-hover:text-accent">
{role.title}
</span>
<span className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1">
<span className="flex items-center gap-1 text-xs text-ink-muted">
<MapPin className="size-3.5" aria-hidden="true" />
{role.location}
</span>
<Badge size="sm">{role.type}</Badge>
</span>
</span>
<ArrowRight
className="size-4 shrink-0 text-ink-subtle transition-transform group-hover:translate-x-0.5 group-hover:text-accent"
aria-hidden="true"
/>
</Link>
</li>
))}
</ul>
</div>
</Container>
</section>
)
}
components/blocks/sections/team/hiring.tsx
import { ArrowRight, MapPin } from 'lucide-react'
import Link from 'next/link'
import { Container } from '@/components/ui/layout'
import { AvatarGroup } from '@/components/ui/avatar'
import { Badge } from '@/components/ui/badge'
import { team } from '@/content/demo'
/**
* Team section with open roles
*
* Combines "who we are" with "we are hiring", because a careers section
* detached from the team it would join reads as a job board. Each role links
* to the application flow that actually exists in this demo.
*/
const roles = [
{ title: 'Frontend engineer, design systems', location: 'Remote — GMT ± 3', type: 'Full time' },
{ title: 'Accessibility specialist', location: 'Remote — Europe', type: 'Full time' },
{ title: 'Documentation engineer', location: 'Remote — worldwide', type: 'Contract' },
]
export default function HiringTeam() {
return (
<section className="border-b border-line bg-canvas py-section">
<Container>
<div className="grid gap-10 lg:grid-cols-[1fr_1.3fr] lg:gap-16">
<div>
<h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
Eight people, three open roles.
</h2>
<p className="mt-4 text-md text-ink-muted">
Small enough that everyone touches the component set, large enough that nobody is the
only person who understands a part of it.
</p>
<AvatarGroup
names={team.map((member) => member.name)}
size="md"
max={6}
label="The Foundry team"
className="mt-6"
/>
</div>
<ul className="divide-y divide-[var(--color-border-subtle)] rounded-xl border border-line bg-surface">
{roles.map((role) => (
<li key={role.title}>
<Link
href="/forms/application"
className="group flex items-center gap-4 p-5 transition-colors hover:bg-surface-sunken"
>
<span className="min-w-0 flex-1">
<span className="block text-sm font-semibold text-ink-strong group-hover:text-accent">
{role.title}
</span>
<span className="mt-1 flex flex-wrap items-center gap-x-3 gap-y-1">
<span className="flex items-center gap-1 text-xs text-ink-muted">
<MapPin className="size-3.5" aria-hidden="true" />
{role.location}
</span>
<Badge size="sm">{role.type}</Badge>
</span>
</span>
<ArrowRight
className="size-4 shrink-0 text-ink-subtle transition-transform group-hover:translate-x-0.5 group-hover:text-accent"
aria-hidden="true"
/>
</Link>
</li>
))}
</ul>
</div>
</Container>
</section>
)
}
components/ui/avatar.tsx
import type { ReactNode } from 'react'
import { cn } from '@/lib/cn'
import { initials as toInitials } from '@/lib/format'
/**
* Avatar / AvatarGroup
*
* Foundry ships no photography, so avatars render deterministic initials on a
* tinted surface. The tint is derived from the name's character codes, which
* keeps the same person the same colour on every page without a colour field
* in the data.
*
* A decorative avatar next to a visible name is `aria-hidden`; a standalone
* one exposes the name as its label.
*/
export interface AvatarProps {
name: string
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl'
/** Suppresses the accessible name when the name is already on screen. */
decorative?: boolean
/** Small badge anchored bottom-right, e.g. a presence dot. */
indicator?: ReactNode
shape?: 'circle' | 'square'
className?: string
}
const sizes = {
xs: 'size-5 text-2xs',
sm: 'size-7 text-2xs',
md: 'size-9 text-xs',
lg: 'size-12 text-sm',
xl: 'size-16 text-lg',
} as const
const tints = [
'bg-accent-soft text-accent-soft-ink',
'bg-success-soft text-success',
'bg-warning-soft text-warning',
'bg-info-soft text-info',
'bg-danger-soft text-danger',
'bg-surface-sunken text-ink-muted',
] as const
function tintFor(name: string): string {
let hash = 0
for (let i = 0; i < name.length; i += 1) hash = (hash * 31 + name.charCodeAt(i)) % 997
return tints[hash % tints.length] ?? tints[0]
}
export function Avatar({
name,
size = 'md',
decorative = false,
indicator,
shape = 'circle',
className,
}: AvatarProps) {
return (
<span className={cn('relative inline-flex shrink-0', className)}>
<span
role={decorative ? undefined : 'img'}
aria-label={decorative ? undefined : name}
aria-hidden={decorative || undefined}
className={cn(
'inline-flex items-center justify-center border border-line font-semibold select-none',
shape === 'circle' ? 'rounded-full' : 'rounded-md',
sizes[size],
tintFor(name),
)}
>
{toInitials(name)}
</span>
{indicator ? <span className="absolute -right-0.5 -bottom-0.5">{indicator}</span> : null}
</span>
)
}
export interface AvatarGroupProps {
names: string[]
size?: AvatarProps['size']
/** Names beyond this count collapse into a "+n" chip. */
max?: number
className?: string
label?: string
}
export function AvatarGroup({ names, size = 'sm', max = 4, className, label }: AvatarGroupProps) {
const visible = names.slice(0, max)
const overflow = names.length - visible.length
return (
<span
className={cn('flex items-center', className)}
role="group"
aria-label={label ?? `${names.length} people`}
>
{visible.map((name) => (
<span
key={name}
className="-ml-2 first:ml-0 ring-2 ring-[var(--color-surface)] rounded-full"
>
<Avatar name={name} size={size} decorative />
</span>
))}
{overflow > 0 ? (
<span
className={cn(
'-ml-2 inline-flex items-center justify-center rounded-full border border-line bg-surface-sunken font-semibold text-ink-muted ring-2 ring-[var(--color-surface)]',
sizes[size],
)}
>
+{overflow}
</span>
) : null}
<span className="sr-only">{names.join(', ')}</span>
</span>
)
}
Demo source — adapt to your project. Foundry is not published as a package.
Usage
A careers section detached from the team it would join reads as a job board. Each role links to the application flow that actually exists in this demo.
- Name the location and contract type on every role — omitting them costs applications.
- Link to a real application flow, not a generic contact form.
Variants and states
Every entry below is a genuine difference in behaviour or layout, and every one of them is visible in the preview above.
- Avatar group
- Three roles
- Full-row links
Accessibility
- Group avatar
- The team is announced once as a labelled group.
- Whole-row links
- Each role is a single focus stop with a descriptive name.
Foundry implements published ARIA patterns and is tested against them. No WCAG certification is claimed — see the accessibility documentation for what is and is not covered.
Related
All sectionsJob application
One open question instead of a cover letter, with an optional CV and no demographic fields.
starter4 variantsTeam list
Hairline rows that align names and roles into scannable columns.
starter3 variantsMulti-step application
Eligibility first, so nobody completes twenty questions before discovering they cannot apply.
advanced4 variants