Menu

Change language

GitHub repo

Team Section

blockmarketing

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.

Installation

$npx shadcn@latest add https://uipkge.dev/r/lit/team-section.json
Manifest:https://uipkge.dev/r/lit/team-section.jsonInstalls to:src/uip/blocks/team-section/

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
eyebrowstringOur teamoptional
headingstringThe people behind the productoptional
descriptionstringA small team with strong opinions about craft. We design, build and support everything ourselves — no outsourcing, no black boxes.optional
membersTeamMember[]DEFAULT_MEMBERSoptional
variant'grid' | 'compact'gridoptional

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

TeamMember
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
}
MemberSection
interface MemberSection {
  department?: string
  members: TeamMember[]
}
HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-team-section': UipTeamSection
  }
}

npm dependencies

Files installed (1)

  • src/uip/blocks/team-section/team-section.ts11.3 kB
    import { LitElement, css, html } from 'lit'
    import { cn } from '../../lib/utils'
    import { tailwind } from '../../lib/styles'
    import { ThemeController } from '../../lib/theme'
    // Side-effect imports: register the composed elements for standalone use
    // (consumers who install only this block still get working primitives).
    import '../../components/avatar/avatar'
    import '../../components/badge/badge'
    import '../../components/button/button'
    import '../../components/card/card'
    
    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',
      },
    ]
    
    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,
      }))
    }
    
    const XIcon = (className: string) => html`<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" class=${className}>
      <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>`
    
    const GithubIcon = (className: string) => html`<svg
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
      class=${className}
    >
      <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>`
    
    const LinkedinIcon = (className: string) => html`<svg
      viewBox="0 0 24 24"
      fill="currentColor"
      aria-hidden="true"
      class=${className}
    >
      <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>`
    
    /**
     * <uip-team-section> — the registry TeamSection block as a web component.
     *
     * Class strings are React's TeamSection verbatim. `heading` is React's `title`
     * (`title` is taken by HTMLElement); `eyebrow`, `description`, `members` and
     * `variant` keep React's names (`members` is set as a property — complex
     * objects can't come from attributes). Social links render as <uip-button>
     * anchors (React's asChild pattern); `rel="noreferrer"` has no forwarding
     * target on uip-button and is omitted.
     */
    export class UipTeamSection extends LitElement {
      static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }
      // :host display can't be a utility class (the host has no template of its own).
      static styles = [tailwind, css`:host { display: block; }`]
    
      static properties = {
        eyebrow: {},
        heading: {},
        description: {},
        members: {},
        variant: { reflect: true },
      }
    
      eyebrow = 'Our team'
      heading = '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: TeamMember[] = DEFAULT_MEMBERS
      variant: 'grid' | 'compact' = 'grid'
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'team-section')
      }
    
      render() {
        const compact = this.variant === 'compact'
        const showHeader = Boolean(this.eyebrow || this.heading || this.description)
        const sections = getSections(this.members ?? DEFAULT_MEMBERS)
    
        return html`
          <section data-slot="team-section" class="bg-background">
            <div class="mx-auto max-w-6xl px-6 py-24">
              ${showHeader
                ? html`
                    <div data-slot="team-section-header" class="mx-auto max-w-2xl text-center">
                      ${this.eyebrow
                        ? html`<p class="text-primary text-sm font-medium tracking-widest uppercase">${this.eyebrow}</p>`
                        : ''}
                      ${this.heading
                        ? html`<h2 class="mt-2 text-3xl font-semibold tracking-tight sm:text-4xl">${this.heading}</h2>`
                        : ''}
                      ${this.description
                        ? html`<p class="text-muted-foreground mt-3 text-lg leading-relaxed">${this.description}</p>`
                        : ''}
                    </div>
                  `
                : ''}
              ${sections.map(
                (section, i) => html`
                  <div
                    data-slot="team-section-group"
                    class=${i > 0 || showHeader ? 'mt-12' : ''}
                  >
                    ${section.department
                      ? html`<h3 class="text-muted-foreground text-xs font-semibold tracking-widest uppercase">
                          ${section.department}
                        </h3>`
                      : ''}
                    <div
                      data-slot="team-section-grid"
                      class=${cn(
                        'grid gap-6 sm:grid-cols-2 lg:grid-cols-3',
                        compact ? 'gap-4' : '',
                        section.department ? 'mt-6' : '',
                      )}
                    >
                      ${section.members.map(
                        (member) => html`
                          <uip-card class="text-center">
                            <uip-card-content class=${cn('flex flex-col items-center', compact ? 'gap-3 p-4' : 'gap-4 p-6')}>
                              <uip-avatar class=${compact ? '[&::part(base)]:size-10' : '[&::part(base)]:size-16'}>
                                <uip-avatar-fallback class=${compact ? '[&::part(base)]:text-sm' : '[&::part(base)]:text-lg'}>
                                  ${initialsFor(member.name)}
                                </uip-avatar-fallback>
                              </uip-avatar>
                              <div class="space-y-1">
                                <p class="text-sm font-semibold">${member.name}</p>
                                <uip-badge variant="secondary">${member.role}</uip-badge>
                                ${!compact && member.bio
                                  ? html`<p class="text-muted-foreground line-clamp-2 text-xs leading-relaxed">
                                      ${member.bio}
                                    </p>`
                                  : ''}
                              </div>
                              ${member.twitter || member.github || member.linkedin
                                ? html`
                                    <div class="flex items-center gap-0.5">
                                      ${member.twitter
                                        ? html`<uip-button
                                            variant="ghost"
                                            size="icon-sm"
                                            href=${member.twitter}
                                            target="_blank"
                                            aria-label=${`${member.name} on X (Twitter)`}
                                          >
                                            ${XIcon('size-4')}
                                          </uip-button>`
                                        : ''}
                                      ${member.github
                                        ? html`<uip-button
                                            variant="ghost"
                                            size="icon-sm"
                                            href=${member.github}
                                            target="_blank"
                                            aria-label=${`${member.name} on GitHub`}
                                          >
                                            ${GithubIcon('size-4')}
                                          </uip-button>`
                                        : ''}
                                      ${member.linkedin
                                        ? html`<uip-button
                                            variant="ghost"
                                            size="icon-sm"
                                            href=${member.linkedin}
                                            target="_blank"
                                            aria-label=${`${member.name} on LinkedIn`}
                                          >
                                            ${LinkedinIcon('size-4')}
                                          </uip-button>`
                                        : ''}
                                    </div>
                                  `
                                : ''}
                            </uip-card-content>
                          </uip-card>
                        `,
                      )}
                    </div>
                  </div>
                `,
              )}
            </div>
          </section>
        `
      }
    }
    
    customElements.get('uip-team-section') || customElements.define('uip-team-section', UipTeamSection)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-team-section': UipTeamSection
      }
    }
    

Raw manifest:https://uipkge.dev/r/lit/team-section.json