Skip to content

Leadership feature

Three people with a statement each, for an about page where trust is the question.

Contentstarterteamleadershipaboutstatements

Live preview

full widthLive preview — open it in a new tab for the full-height version.
Open the preview in a new tab

Source

This exact file renders the preview above.

import { Container } from '@/components/ui/layout'
import { Avatar } from '@/components/ui/avatar'
import { team } from '@/content/demo'

/**
 * Leadership feature
 *
 * Three people, given room for a real paragraph each. Appropriate for an about
 * page where the reader is deciding whether to trust the organisation, and a
 * name plus a job title is not enough to do that.
 */
const leaders = team.slice(0, 3).map((member, index) => ({
  ...member,
  statement: [
    'Most component libraries fail at the second product, not the first. Everything here is designed for the second one.',
    'Accessibility work that happens at review time is always late. It belongs in the primitive.',
    'Performance problems are usually architecture problems wearing a different hat.',
  ][index],
}))

export default function LeadershipTeam() {
  return (
    <section className="border-b border-line bg-surface-sunken py-section">
      <Container>
        <div className="max-w-2xl">
          <h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
            The people who decided how this works.
          </h2>
        </div>

        <ul className="mt-12 grid gap-8 lg:grid-cols-3">
          {leaders.map((leader) => (
            <li key={leader.name} className="flex flex-col">
              <div className="flex items-center gap-3.5">
                <Avatar name={leader.name} size="lg" decorative />
                <div>
                  <p className="text-sm font-semibold text-ink-strong">{leader.name}</p>
                  <p className="text-xs text-accent">{leader.role}</p>
                </div>
              </div>
              <blockquote className="mt-5 border-l-2 border-accent-line pl-4 text-sm leading-relaxed text-ink">
                “{leader.statement}”
              </blockquote>
              <p className="mt-4 text-xs leading-relaxed text-ink-muted">{leader.bio}</p>
            </li>
          ))}
        </ul>
      </Container>
    </section>
  )
}

components/blocks/sections/team/leadership.tsx

import { Container } from '@/components/ui/layout'
import { Avatar } from '@/components/ui/avatar'
import { team } from '@/content/demo'

/**
 * Leadership feature
 *
 * Three people, given room for a real paragraph each. Appropriate for an about
 * page where the reader is deciding whether to trust the organisation, and a
 * name plus a job title is not enough to do that.
 */
const leaders = team.slice(0, 3).map((member, index) => ({
  ...member,
  statement: [
    'Most component libraries fail at the second product, not the first. Everything here is designed for the second one.',
    'Accessibility work that happens at review time is always late. It belongs in the primitive.',
    'Performance problems are usually architecture problems wearing a different hat.',
  ][index],
}))

export default function LeadershipTeam() {
  return (
    <section className="border-b border-line bg-surface-sunken py-section">
      <Container>
        <div className="max-w-2xl">
          <h2 className="display-type text-2xl font-semibold text-ink-strong sm:text-3xl">
            The people who decided how this works.
          </h2>
        </div>

        <ul className="mt-12 grid gap-8 lg:grid-cols-3">
          {leaders.map((leader) => (
            <li key={leader.name} className="flex flex-col">
              <div className="flex items-center gap-3.5">
                <Avatar name={leader.name} size="lg" decorative />
                <div>
                  <p className="text-sm font-semibold text-ink-strong">{leader.name}</p>
                  <p className="text-xs text-accent">{leader.role}</p>
                </div>
              </div>
              <blockquote className="mt-5 border-l-2 border-accent-line pl-4 text-sm leading-relaxed text-ink">
                “{leader.statement}”
              </blockquote>
              <p className="mt-4 text-xs leading-relaxed text-ink-muted">{leader.bio}</p>
            </li>
          ))}
        </ul>
      </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 name plus a job title is not enough for a reader deciding whether to trust an organisation. Each person gets a real statement about how they work.

  • Statements should be opinions, not slogans.
  • Three people. A leadership section with nine is an org chart.

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.

  • Three columns
  • Personal statement
  • Bio line

Accessibility

Quote semantics
Statements are blockquotes attributed to the person above them.
Heading structure
Names are not headings, so they do not compete with the section heading.

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.