Menu

Change language

GitHub repo

Careers Job Board

blockmarketing

Careers page and job openings board with perks overview, live department & location filtering, keyword search, grouped department openings, and general application CTA.

Installation

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

Variants

Loading interactive previews…

Props

NameType / ValuesDefaultRequired
headingstringJoin our teamoptional
subtitlestringHelp us build the open-source UI infrastructure of tomorrowoptional

Schema

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

JobOpening
interface JobOpening {
  id: string
  title: string
  department: 'Engineering' | 'Design' | 'Product' | 'Marketing'
  location: 'Remote' | 'San Francisco, CA' | 'London, UK'
  type: string
  level: string
  salary: string
  description: string
}
Perk
interface Perk {
  icon: IconNode
  iconName: string
  title: string
  description: string
}
HTMLElementTagNameMap
interface HTMLElementTagNameMap {
    'uip-careers-job-board': UipCareersJobBoard
  }
}

Files installed (1)

  • src/uip/blocks/careers-job-board/careers-job-board.ts17.9 kB
    import { LitElement, css, html, nothing } from 'lit'
    import {
      ArrowRight,
      Briefcase,
      Coins,
      Globe,
      GraduationCap,
      HeartHandshake,
      MapPin,
      Search,
      SearchX,
      TrendingUp,
      type IconNode,
    } from 'lucide'
    import { cn } from '../../lib/utils'
    import { tailwind } from '../../lib/styles'
    import { ThemeController } from '../../lib/theme'
    import { icon } from '../../lib/icon'
    // Side-effect imports: register the composed elements for standalone use
    // (consumers who install only this block still get working primitives).
    import '../../components/badge/badge'
    import '../../components/button/button'
    import '../../components/input/input'
    import type { UipInput } from '../../components/input/input'
    import '../../components/select/select'
    import type { UipSelect } from '../../components/select/select'
    
    interface JobOpening {
      id: string
      title: string
      department: 'Engineering' | 'Design' | 'Product' | 'Marketing'
      location: 'Remote' | 'San Francisco, CA' | 'London, UK'
      type: string
      level: string
      salary: string
      description: string
    }
    
    interface Perk {
      icon: IconNode
      iconName: string
      title: string
      description: string
    }
    
    const PERKS: Perk[] = [
      {
        icon: Globe,
        iconName: 'globe',
        title: 'Remote-first',
        description: 'Work from anywhere in the world with flexible hours.',
      },
      {
        icon: GraduationCap,
        iconName: 'graduation-cap',
        title: '$5k learning budget',
        description: 'Annual stipend for courses, books, tools, and conferences.',
      },
      {
        icon: HeartHandshake,
        iconName: 'heart-handshake',
        title: '100% health coverage',
        description: 'Comprehensive medical, dental, and vision for you and dependents.',
      },
      {
        icon: TrendingUp,
        iconName: 'trending-up',
        title: 'Equity packages',
        description: 'Meaningful ownership stake with transparent vesting schedules.',
      },
    ]
    
    const DEPARTMENTS = [
      { id: 'all', label: 'All', count: 12 },
      { id: 'Engineering', label: 'Engineering', count: 6 },
      { id: 'Design', label: 'Design', count: 2 },
      { id: 'Product', label: 'Product', count: 2 },
      { id: 'Marketing', label: 'Marketing', count: 2 },
    ] as const
    
    const LOCATIONS = [
      { id: 'all', label: 'All Locations' },
      { id: 'Remote', label: 'Remote' },
      { id: 'San Francisco, CA', label: 'San Francisco, CA' },
      { id: 'London, UK', label: 'London, UK' },
    ] as const
    
    const JOBS: JobOpening[] = [
      {
        id: 'eng-1',
        title: 'Staff Frontend Engineer',
        department: 'Engineering',
        location: 'Remote',
        type: 'Full-time',
        level: 'Staff',
        salary: '$190k - $240k · 0.3% - 0.6%',
        description:
          'Lead the architecture of our high-performance UI components, compiler toolchains, and multi-framework design systems.',
      },
      {
        id: 'eng-2',
        title: 'Senior Full-Stack Engineer',
        department: 'Engineering',
        location: 'San Francisco, CA',
        type: 'Full-time',
        level: 'Senior',
        salary: '$160k - $210k · 0.25% - 0.5%',
        description:
          'Build end-to-end features spanning serverless edge runtimes, component registries, and developer tooling.',
      },
      {
        id: 'eng-3',
        title: 'Systems & Performance Engineer',
        department: 'Engineering',
        location: 'Remote',
        type: 'Full-time',
        level: 'Senior',
        salary: '$170k - $220k · 0.25% - 0.5%',
        description:
          'Optimize bundling pipelines, AST transformers, and static site generation latency across large workspaces.',
      },
      {
        id: 'eng-4',
        title: 'Design Systems Engineer',
        department: 'Engineering',
        location: 'London, UK',
        type: 'Full-time',
        level: 'Mid-Senior',
        salary: '£110k - £140k · 0.2% - 0.4%',
        description:
          'Bridge design and code by maintaining tokens, accessible headless primitives, and fluid interaction standards.',
      },
      {
        id: 'eng-5',
        title: 'Developer Experience Engineer',
        department: 'Engineering',
        location: 'Remote',
        type: 'Full-time',
        level: 'Mid-Senior',
        salary: '$150k - $190k · 0.2% - 0.4%',
        description:
          'Craft exceptional CLI workflows, interactive documentation playgrounds, and starter template engines.',
      },
      {
        id: 'eng-6',
        title: 'QA & Automation Engineer',
        department: 'Engineering',
        location: 'London, UK',
        type: 'Full-time',
        level: 'Mid-Senior',
        salary: '£95k - £125k · 0.15% - 0.3%',
        description:
          'Expand cross-browser visual regression testing suites, accessibility audits, and automated release checks.',
      },
      {
        id: 'des-1',
        title: 'Senior Product Designer',
        department: 'Design',
        location: 'San Francisco, CA',
        type: 'Full-time',
        level: 'Senior',
        salary: '$155k - $195k · 0.25% - 0.45%',
        description:
          'Define micro-interactions, responsive dashboard workflows, and layout ergonomics across our block catalog.',
      },
      {
        id: 'des-2',
        title: 'Brand & Motion Designer',
        department: 'Design',
        location: 'Remote',
        type: 'Full-time',
        level: 'Mid-Senior',
        salary: '$140k - $180k · 0.2% - 0.35%',
        description:
          'Create fluid motion principles, brand identity assets, and polished launch visuals for major releases.',
      },
      {
        id: 'prd-1',
        title: 'Staff Technical Product Manager',
        department: 'Product',
        location: 'Remote',
        type: 'Full-time',
        level: 'Staff',
        salary: '$180k - $230k · 0.3% - 0.5%',
        description:
          'Drive product roadmap for developer workflows, unbundled registry distribution, and monorepo orchestration.',
      },
      {
        id: 'prd-2',
        title: 'Product Operations Lead',
        department: 'Product',
        location: 'San Francisco, CA',
        type: 'Full-time',
        level: 'Senior',
        salary: '$145k - $185k · 0.2% - 0.35%',
        description: 'Manage release triage, community feature requests, and cross-team execution cadences.',
      },
      {
        id: 'mkt-1',
        title: 'Developer Advocate / Technical Writer',
        department: 'Marketing',
        location: 'Remote',
        type: 'Full-time',
        level: 'Mid-Senior',
        salary: '$140k - $180k · 0.2% - 0.4%',
        description:
          'Author technical deep-dives, create interactive video tutorials, and engage with the open-source community.',
      },
      {
        id: 'mkt-2',
        title: 'Growth Marketing Lead',
        department: 'Marketing',
        location: 'London, UK',
        type: 'Full-time',
        level: 'Senior',
        salary: '£105k - £135k · 0.2% - 0.4%',
        description:
          'Lead developer-focused growth experiments, SEO architecture, launch campaigns, and analytics instrumentation.',
      },
    ]
    
    /**
     * <uip-careers-job-board> — the registry CareersJobBoard block as a web component.
     *
     * Class strings are React's CareersJobBoard verbatim. `heading` is React's
     * `title` (`title` is taken by HTMLElement). React state (useState) becomes
     * @state fields. The search icon moves from React's absolute overlay into
     * <uip-input>'s prefix slot (same visual result, no overlay positioning to
     * maintain), so the input keeps `text-sm` and drops the overlay's `pl-9`.
     * The location Select is a <uip-select> reading light-DOM <option> children.
     */
    export class UipCareersJobBoard 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 = {
        heading: {},
        subtitle: {},
        selectedDepartment: { state: true },
        selectedLocation: { state: true },
        searchQuery: { state: true },
      }
    
      heading = 'Join our team'
      subtitle = 'Help us build the open-source UI infrastructure of tomorrow'
      private selectedDepartment = 'all'
      private selectedLocation = 'all'
      private searchQuery = ''
    
      constructor() {
        super()
        new ThemeController(this)
      }
    
      connectedCallback() {
        super.connectedCallback()
        this.setAttribute('data-uipkge', '')
        this.setAttribute('data-slot', 'careers-job-board')
      }
    
      private get filteredJobs() {
        const q = this.searchQuery.trim().toLowerCase()
        return JOBS.filter((job) => {
          const matchDept = this.selectedDepartment === 'all' || job.department === this.selectedDepartment
          const matchLoc = this.selectedLocation === 'all' || job.location === this.selectedLocation
          if (!matchDept || !matchLoc) return false
          if (!q) return true
          return (
            job.title.toLowerCase().includes(q) ||
            job.description.toLowerCase().includes(q) ||
            job.department.toLowerCase().includes(q) ||
            job.level.toLowerCase().includes(q) ||
            job.location.toLowerCase().includes(q) ||
            job.type.toLowerCase().includes(q) ||
            job.salary.toLowerCase().includes(q)
          )
        })
      }
    
      private get departmentGroups() {
        const allDepts = ['Engineering', 'Design', 'Product', 'Marketing'] as const
        const targetDepartments =
          this.selectedDepartment === 'all' ? allDepts : allDepts.filter((dept) => dept === this.selectedDepartment)
        return targetDepartments
          .map((dept) => ({
            name: dept,
            jobs: this.filteredJobs.filter((job) => job.department === dept),
          }))
          .filter((group) => group.jobs.length > 0)
      }
    
      private get isFiltered() {
        return this.selectedDepartment !== 'all' || this.selectedLocation !== 'all' || this.searchQuery.trim() !== ''
      }
    
      private resetFilters() {
        this.selectedDepartment = 'all'
        this.selectedLocation = 'all'
        this.searchQuery = ''
      }
    
      render() {
        const groups = this.departmentGroups
        return html`
          <section data-slot="careers-job-board" class=${cn('w-full space-y-12 py-6')}>
            <div class="mx-auto max-w-3xl space-y-4 text-center">
              <uip-badge variant="secondary" class="gap-1.5 px-3 py-1 text-xs font-medium">
                ${icon(Briefcase, 'briefcase', 'text-primary size-3.5')} We're hiring
              </uip-badge>
              <h1 class="text-foreground text-3xl font-bold tracking-tight sm:text-4xl lg:text-5xl">${this.heading}</h1>
              <p class="text-muted-foreground text-base leading-relaxed sm:text-lg">${this.subtitle}</p>
            </div>
    
            <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
              ${PERKS.map(
                (perk) => html`<div class="bg-card border-border flex flex-col gap-3 rounded-xl border p-4 shadow-xs">
                  <div class="bg-primary/10 text-primary flex size-9 items-center justify-center rounded-lg">
                    ${icon(perk.icon, perk.iconName, 'size-4.5')}
                  </div>
                  <div class="space-y-1">
                    <p class="text-foreground text-sm font-semibold">${perk.title}</p>
                    <p class="text-muted-foreground text-xs leading-relaxed">${perk.description}</p>
                  </div>
                </div>`,
              )}
            </div>
    
            <div class="bg-card border-border space-y-4 rounded-xl border p-4 shadow-xs">
              <div class="flex flex-wrap items-center gap-1.5">
                ${DEPARTMENTS.map(
                  (dept) => html`<uip-button
                    variant=${this.selectedDepartment === dept.id ? 'default' : 'outline'}
                    size="sm"
                    class="h-8 rounded-lg text-xs"
                    @click=${() => {
                      this.selectedDepartment = dept.id
                    }}
                  >
                    ${dept.label}
                    <span
                      class=${cn(
                        'ml-1.5 rounded-full px-1.5 py-0.5 text-xs font-medium tabular-nums',
                        this.selectedDepartment === dept.id
                          ? 'bg-primary-foreground/20 text-primary-foreground'
                          : 'bg-muted text-muted-foreground',
                      )}
                    >
                      ${dept.count}
                    </span>
                  </uip-button>`,
                )}
              </div>
    
              <div class="flex flex-col gap-3 sm:flex-row sm:items-center">
                <uip-input
                  class="flex-1 text-sm"
                  .value=${this.searchQuery}
                  placeholder="Search roles, technologies, or keywords..."
                  aria-label="Search open positions"
                  @input=${(e: Event) => {
                    this.searchQuery = (e.target as UipInput).value
                  }}
                >
                  <span slot="prefix">${icon(Search, 'search', 'size-4')}</span>
                </uip-input>
    
                <div class="w-full shrink-0 sm:w-56">
                  <uip-select
                    class="w-full text-xs sm:text-sm"
                    placeholder="All Locations"
                    aria-label="Filter by location"
                    .value=${this.selectedLocation}
                    @change=${(e: Event) => {
                      this.selectedLocation = (e.target as UipSelect).value
                    }}
                  >
                    ${LOCATIONS.map((loc) => html`<option value=${loc.id}>${loc.label}</option>`)}
                  </uip-select>
                </div>
    
                ${this.isFiltered
                  ? html`<uip-button
                      variant="ghost"
                      size="sm"
                      class="text-muted-foreground hover:text-foreground text-xs"
                      @click=${this.resetFilters}
                    >
                      Reset filters
                    </uip-button>`
                  : nothing}
              </div>
            </div>
    
            ${groups.length > 0
              ? html`<div class="space-y-10">
                  ${groups.map(
                    (group) => html`<div class="space-y-4">
                      <div class="border-border flex items-center justify-between border-b pb-3">
                        <div class="flex items-center gap-2.5">
                          <h2 class="text-foreground text-lg font-semibold tracking-tight">${group.name}</h2>
                          <uip-badge variant="secondary" class="text-xs font-normal">
                            ${group.jobs.length} ${group.jobs.length === 1 ? 'opening' : 'openings'}
                          </uip-badge>
                        </div>
                      </div>
    
                      <div class="space-y-3">
                        ${group.jobs.map(
                          (job) => html`<article
                            class="group bg-card border-border hover:border-primary/40 relative flex flex-col justify-between gap-4 rounded-xl border p-5 shadow-xs transition-colors duration-150 hover:shadow-sm md:flex-row md:items-center"
                          >
                            <div class="min-w-0 flex-1 space-y-2">
                              <div class="flex flex-wrap items-center gap-2">
                                <h3
                                  class="group-hover:text-primary text-foreground text-base font-semibold tracking-tight transition-colors sm:text-lg"
                                >
                                  ${job.title}
                                </h3>
                                <uip-badge variant="outline" class="text-xs">${job.level}</uip-badge>
                              </div>
                              <p class="text-muted-foreground line-clamp-2 text-sm leading-relaxed">${job.description}</p>
                              <div class="text-muted-foreground flex flex-wrap items-center gap-x-4 gap-y-2 pt-1 text-xs">
                                <span class="inline-flex items-center gap-1.5">
                                  ${icon(MapPin, 'map-pin', 'text-muted-foreground size-3.5 shrink-0')} ${job.location}
                                </span>
                                <span class="inline-flex items-center gap-1.5">
                                  ${icon(Briefcase, 'briefcase', 'text-muted-foreground size-3.5 shrink-0')} ${job.type}
                                </span>
                                <span class="text-foreground inline-flex items-center gap-1.5 font-medium">
                                  ${icon(Coins, 'coins', 'text-muted-foreground size-3.5 shrink-0')} ${job.salary}
                                </span>
                              </div>
                            </div>
                            <div class="shrink-0 pt-2 md:pt-0">
                              <uip-button class="w-full gap-1.5 md:w-auto" size="sm">
                                Apply now ${icon(ArrowRight, 'arrow-right', 'size-3.5 transition-transform group-hover:translate-x-0.5')}
                              </uip-button>
                            </div>
                          </article>`,
                        )}
                      </div>
                    </div>`,
                  )}
                </div>`
              : html`<div
                  class="border-border flex flex-col items-center justify-center rounded-xl border border-dashed p-12 text-center"
                >
                  <div class="bg-muted text-muted-foreground mb-4 flex size-12 items-center justify-center rounded-full">
                    ${icon(SearchX, 'search-x', 'size-6')}
                  </div>
                  <h3 class="text-foreground text-base font-semibold">No matching positions found</h3>
                  <p class="text-muted-foreground mt-1 mb-4 max-w-sm text-sm leading-normal">
                    We could not find any roles matching your current search or filters. Try adjusting your criteria.
                  </p>
                  <uip-button variant="outline" size="sm" @click=${this.resetFilters}>Clear all filters</uip-button>
                </div>`}
    
            <div
              class="bg-card border-border flex flex-col items-start justify-between gap-6 rounded-2xl border p-6 shadow-xs sm:flex-row sm:items-center sm:p-8"
            >
              <div class="max-w-xl space-y-1.5">
                <h3 class="text-foreground text-lg font-semibold tracking-tight">Don't see your role?</h3>
                <p class="text-muted-foreground text-sm leading-relaxed">
                  We are always looking for curious, high-craft engineers, designers, and thinkers. Send us an open
                  application and tell us what you'd love to build with us.
                </p>
              </div>
              <uip-button variant="outline" class="shrink-0 gap-2">
                Send open application ${icon(ArrowRight, 'arrow-right', 'size-4')}
              </uip-button>
            </div>
          </section>
        `
      }
    }
    
    customElements.get('uip-careers-job-board') || customElements.define('uip-careers-job-board', UipCareersJobBoard)
    
    declare global {
      interface HTMLElementTagNameMap {
        'uip-careers-job-board': UipCareersJobBoard
      }
    }
    

Raw manifest:https://uipkge.dev/r/lit/careers-job-board.json