Vue componentFree

Team Grid

Vue 3 team section with a responsive grid of member cards: initials avatars, role, short bio, location and social links. Tailwind CSS, dark mode.

by Vueplayv1.0.0Updated 3 hours ago

Install
Full screen ↗

About this component

An about-page team section that looks finished before you have headshots. Each card shows a gradient initials avatar, the person's name and role, a one or two sentence bio, where they are based and a row of icon links.

Links use a small set of neutral glyphs (website, email, code profile, chat) and every icon link gets an accessible name like "Website of Nadia Osei". The grid moves from one column on phones to two and then three, and cards in a row share the same height.

Pass your members array to replace the sample team. The after slot adds a full-width row below the grid, which works well for a hiring call to action.

  • Initials avatars with stable gradients
  • Role, bio and location per member
  • Labelled icon links
  • Full-width after slot for hiring CTAs
  • Light and dark mode

Code

11 files in the repo
<script setup lang="ts">
export type MemberLinkType = 'website' | 'email' | 'code' | 'chat'

export interface MemberLink {
  type: MemberLinkType
  href: string
}

export interface TeamMember {
  name: string
  role: string
  bio: string
  location?: string
  links?: MemberLink[]
}

withDefaults(defineProps<{
  eyebrow?: string
  heading?: string
  subheading?: string
  members?: TeamMember[]
}>(), {
  eyebrow: 'Our team',
  heading: 'The people behind Orchard',
  subheading: 'A small, remote team across five time zones. We care about calm software, clear writing and answering support tickets ourselves.',
  members: () => [
    { name: 'Nadia Osei', role: 'Co-founder & CEO', bio: 'Ran operations at two logistics startups before deciding scheduling software could be kinder.', location: 'Lisbon', links: [{ type: 'website', href: '#nadia' }, { type: 'email', href: '#nadia-mail' }] },
    { name: 'Lukas Brenner', role: 'Co-founder & CTO', bio: 'Writes most of the sync engine and all of the terrible puns in our release notes.', location: 'Vienna', links: [{ type: 'code', href: '#lukas-code' }, { type: 'website', href: '#lukas' }] },
    { name: 'Hana Moriyama', role: 'Head of Design', bio: 'Obsessed with empty states, keyboard flows and the space between two buttons.', location: 'Osaka', links: [{ type: 'website', href: '#hana' }, { type: 'chat', href: '#hana-chat' }] },
    { name: 'Rafael Duarte', role: 'Senior Engineer', bio: 'Keeps the API fast and the on-call rotation boring. Former competitive cyclist.', location: 'São Paulo', links: [{ type: 'code', href: '#rafael-code' }] },
    { name: 'Grace Achterberg', role: 'Customer Success', bio: 'Answers most support threads within the hour and turns them into better docs.', location: 'Utrecht', links: [{ type: 'email', href: '#grace-mail' }, { type: 'chat', href: '#grace-chat' }] },
    { name: 'Theo Karlsson', role: 'Product Engineer', bio: 'Ships the small details customers mention in reviews. Bakes sourdough on Fridays.', location: 'Gothenburg', links: [{ type: 'code', href: '#theo-code' }, { type: 'website', href: '#theo' }] },
  ],
})

defineSlots<{
  /** Full-width row after the members, e.g. a hiring call to action. */
  after?: () => unknown
}>()

const linkMeta: Record<MemberLinkType, { label: string, path: string }> = {
  website: { label: 'Website', path: 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18Zm-9-9h18M12 3c2.4 2.5 3.5 5.5 3.5 9s-1.1 6.5-3.5 9c-2.4-2.5-3.5-5.5-3.5-9s1.1-6.5 3.5-9Z' },
  email: { label: 'Email', path: 'M3.5 6.5h17v11h-17v-11Zm0 .5 8.5 6 8.5-6' },
  code: { label: 'Code profile', path: 'm8.5 8-4 4 4 4m7-8 4 4-4 4M13.5 5l-3 14' },
  chat: { label: 'Chat', path: 'M5 5h14v10h-7l-4 3.5V15H5V5Z' },
}

const initials = (name: string) => name.split(/\s+/).filter(Boolean).slice(0, 2).map(p => p[0]!.toUpperCase()).join('')

const gradients = [
  'from-cyan-400 to-cyan-600',
  'from-sky-400 to-indigo-500',
  'from-teal-400 to-cyan-600',
  'from-cyan-500 to-blue-600',
]
function gradient(name: string) {
  let h = 0
  for (const ch of name) h = (h * 31 + ch.charCodeAt(0)) >>> 0
  return gradients[h % gradients.length]
}
</script>

<template>
  <section class="px-4 py-20 sm:px-6 sm:py-24 lg:px-8">
    <div class="mx-auto max-w-6xl">
      <div class="max-w-2xl">
        <p v-if="eyebrow" class="text-sm font-semibold text-cyan-700 dark:text-cyan-400">{{ eyebrow }}</p>
        <h2 class="mt-2 text-3xl font-bold tracking-tight text-balance text-gray-900 sm:text-4xl dark:text-white">{{ heading }}</h2>
        <p class="mt-4 text-lg text-pretty text-gray-600 dark:text-gray-400">{{ subheading }}</p>
      </div>

      <ul role="list" class="mt-14 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
        <li
          v-for="member in members"
          :key="member.name"
          class="flex flex-col rounded-2xl bg-white p-6 shadow-sm ring-1 ring-gray-200 dark:bg-gray-900 dark:shadow-none dark:ring-white/10"
        >
          <div class="flex items-center gap-4">
            <span
              aria-hidden="true"
              class="grid size-14 shrink-0 place-items-center rounded-2xl bg-gradient-to-br text-lg font-semibold text-white shadow-sm"
              :class="gradient(member.name)"
            >{{ initials(member.name) }}</span>
            <div class="min-w-0">
              <h3 class="truncate text-base font-semibold text-gray-900 dark:text-white">{{ member.name }}</h3>
              <p class="text-sm font-medium text-cyan-700 dark:text-cyan-400">{{ member.role }}</p>
            </div>
          </div>
          <p class="mt-4 flex-1 text-sm/6 text-gray-600 dark:text-gray-400">{{ member.bio }}</p>
          <div class="mt-5 flex items-center justify-between gap-4 border-t border-gray-100 pt-4 dark:border-white/5">
            <p v-if="member.location" class="inline-flex items-center gap-1.5 text-sm text-gray-500 dark:text-gray-400">
              <svg viewBox="0 0 20 20" aria-hidden="true" class="size-4 fill-current">
                <path fill-rule="evenodd" d="M10 18s6-5.2 6-10A6 6 0 0 0 4 8c0 4.8 6 10 6 10Zm0-7.5a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5Z" clip-rule="evenodd" />
              </svg>
              <span class="sr-only">Based in</span>
              {{ member.location }}
            </p>
            <span v-else />
            <ul v-if="member.links?.length" role="list" class="flex gap-1">
              <li v-for="link in member.links" :key="link.href">
                <a
                  :href="link.href"
                  class="grid size-8 place-items-center rounded-lg text-gray-500 transition hover:bg-cyan-50 hover:text-cyan-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-cyan-600 dark:text-gray-400 dark:hover:bg-cyan-400/10 dark:hover:text-cyan-300"
                  :aria-label="`${linkMeta[link.type].label} of ${member.name}`"
                  :title="linkMeta[link.type].label"
                >
                  <svg viewBox="0 0 24 24" aria-hidden="true" class="size-4.5" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
                    <path :d="linkMeta[link.type].path" />
                  </svg>
                </a>
              </li>
            </ul>
          </div>
        </li>
        <li v-if="$slots.after" class="sm:col-span-2 lg:col-span-3">
          <slot name="after" />
        </li>
      </ul>
    </div>
  </section>
</template>

Props

PropTypeDefaultDescription
eyebrowstring'Our team'Small label above the heading. Empty string hides it.
headingstring'The people behind Orchard'Section heading.
subheadingstring'A small, remote team…'Text under the heading.
membersTeamMember[]6 sample membersMembers with name, role, bio, optional location and optional links ({ type: 'website' | 'email' | 'code' | 'chat', href }).

Slots

SlotDescription
afterFull-width row after the member cards, e.g. a hiring call to action.
All data display →
Preview of BoardFree

Component · Data display

Board

A flexible board component built for smooth panning, zooming with a grid layout. It supports both mouse and touch input, making it a great fit for whiteboards, diagram tools, mind maps, and workflow editors. Designed for performance and responsiveness, it delivers a fluid experience on desktop and mobile with intuitive gestures and real-time interaction on a scalable canvas.

Preview of Changelog TimelineFree

Component · Data display

Changelog Timeline

Vertical changelog timeline for Vue 3 with version badges, dated releases, typed change tags and filter chips. Built with Tailwind CSS, dark mode.

by Vueplay
Preview of Kanban Board$12

Component · Data display

Kanban Board

Vue 3 kanban board with native drag and drop, keyboard moves, WIP limits and inline add card. v-model board state, Tailwind CSS, dark mode.

by Vueplay
Preview of Stats CardsFree

Component · Data display

Stats Cards

Vue 3 KPI stat cards with trend deltas and tiny inline SVG sparklines, no chart library needed. Tailwind CSS, dark mode, typed props.

by Vueplay

Pro feature

Upgrade to Pro

Upgrade to unlock more of Vueplay. Cancel any time.

  • Custom domains with HTTPS
  • Private projects
  • No "Made with Vueplay" badge
  • GitHub sync

$120/year

Compare plansBilling settings