Retour au catalogue

Careers Values First

Page carrieres mettant les valeurs d'entreprise en avant avant les offres d'emploi.

careersmedium Both Responsive a11y
elegantcorporatesaasuniversalagencystackedgrid
Theme

How to build a values-first careers section in React

A values-first careers section in React shows company values as a 2-column card grid before listing open roles. Each card fades in with a staggered whileInView translate-Y animation via Framer Motion, and a bottom CTA bar with an accent background closes the section.

  • Stack: React + Framer Motion 11 + Lucide React, ~93 lines, zero extra dependencies.
  • Animation: viewport-triggered stagger, each card delays by 0.06s × index via whileInView with once:true.
  • Icons are resolved at runtime from a ICON_MAP record (Shield, Zap, Heart, BookOpen from Lucide).
  • Accessible: semantic h2/h3 headings, color contrast left to CSS tokens, no motion-only meaning.
  • Responsive: the 2-column grid holds at all widths; wraps naturally on narrow viewports via CSS grid.

CareersValuesFirst leads a careers page with what the company stands for before showing open roles. A short headline block is followed by a 2-column card grid where each value gets an icon, a title, and a description. A full-width accent bar at the bottom ties the section to a jobs CTA, giving recruiters a culture pitch and a conversion action in one scroll.

Anatomy

The section has three parts stacked vertically. First, a header block (max-width 640px) with an h2 and a subtitle paragraph, revealed as a single unit. Second, a CSS grid with 2 equal columns and a 1.5rem gap; each card has padding, a rounded border, a card background, and a decorative blurred circle in the top-right corner at 5% opacity. Inside each card: a 48×48px icon container with accent-subtle background, then an h3 title, then a muted paragraph. Third, a flex row with accent background holding an open-positions count on the left and a pill-shaped CTA link on the right.

How it works

Every animated element uses Framer Motion's whileInView with viewport={{ once: true }} so the animation fires once when the element enters the screen and never replays on scroll-up. The header animates as a single block (opacity 0→1, y 20→0, duration 0.6s). Each value card delays by `0.08 + i * 0.06` seconds, creating a natural left-to-right stagger across the grid columns. The CTA bar waits 0.3s, entering after the last card. All transitions share the same custom ease `[0.16, 1, 0.3, 1]`, a fast ease-out that reads as responsive rather than bouncy.

How to build it in React

  1. Define the Value interface and icon map

    Each value object carries an icon key (string), a title, a description, and a color hex used for the decorative background blob. Resolve icon strings to actual Lucide components at the top of the file with a Record lookup so the component prop API stays serializable.

    const ICON_MAP: Record<string, LucideIcon> = {
      shield: Shield,
      zap: Zap,
      heart: Heart,
      "book-open": BookOpen,
    };
  2. Animate the header as one block

    Wrap the h2 + subtitle in a single motion.div with initial={{ opacity: 0, y: 20 }} and whileInView={{ opacity: 1, y: 0 }}. Set viewport={{ once: true }} to prevent replays. A 0.6s duration with the custom ease is enough, no stagger needed here, the block is small.

    <motion.div
      initial={{ opacity: 0, y: 20 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
      transition={{ duration: 0.6, ease: [0.16, 1, 0.3, 1] }}
    >
  3. Stagger the value cards

    Map over the values array and give each motion.div a delay of `0.08 + i * 0.06`. With 4 cards this spreads the entrance over ~0.3s total, which reads as fluid without feeling slow. The decorative blob is a 100px div with borderRadius 50%, positioned absolute at top:-20 right:-20, using the value's color at 5% opacity.

    transition={{ duration: 0.5, delay: 0.08 + i * 0.06, ease: EASE }}
  4. Build the accent CTA bar

    Render a motion.div with display:flex, the accent background, and a 0.3s delay so it enters last. Inside, a flex row holds a left text block (open positions count + tagline) and a pill link on the right with the background color as its own background, creating an inverted button from the section's accent.

    <motion.div
      initial={{ opacity: 0, y: 12 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true }}
      transition={{ duration: 0.5, delay: 0.3, ease: EASE }}
      style={{ background: "var(--color-accent)" }}
    >

When to use it

Use this section at the top of a careers or about page when culture is a differentiator, SaaS companies, agencies, and startups competing for talent on values rather than salary. Place it before the job listings so visitors read why before what. Skip it on transactional job boards or pages where time-to-apply is the priority; in those contexts the extra scroll depth costs conversions.

Used by

  • Stripe, Leads its careers page with operating principles before listing open roles, establishing culture before opportunity.
  • Linear, Values and working style section precedes the job board, emphasizing how the team works over what the roles are.
  • Notion, Displays core values with icons and short descriptions above the open positions list on its careers landing.

FAQ

How do I add a fifth value card without breaking the 2-column grid?

The grid uses `repeat(2, 1fr)` so any number of cards fills it automatically, a fifth card simply starts a new row. If you want it centered on the last row, wrap the grid container in a flex parent and add `justify-items: center`.

Can I add a custom icon not in the ICON_MAP?

Extend the ICON_MAP record with any Lucide icon or your own SVG component, the type is `Record<string, LucideIcon>`, so any component accepting a className or style prop works. The fallback is Shield if the key is not found.

Does whileInView replay every time the element re-enters the viewport?

Not with `viewport={{ once: true }}`, which is what this component uses. The animation fires once on the first intersection and never again. Remove the once flag to make it replay on every scroll-down entry.

How do I wire the CTA link to an actual jobs page anchor?

Replace `href="#"` in the anchor tag with `href="#jobs"` (or whatever id you give your listings section). Pass `ctaText` and `openPositionsCount` as props from your data source so the bar reflects live vacancy counts.

"use client";

import { motion } from "framer-motion";
import { Shield, Zap, Heart, BookOpen, ArrowRight } from "lucide-react";
import type { LucideIcon } from "lucide-react";

interface Value {
  icon: string;
  title: string;
  description: string;
  color: string;
}

interface CareersValuesFirstProps {
  title?: string;
  subtitle?: string;
  values?: Value[];
  ctaText?: string;
  openPositionsCount?: number;
}

const EASE = [0.16, 1, 0.3, 1] as const;

Code complet réservé à Pro

Code source intégral, export multi-framework et playground.

Passer en Pro, 9,99€/mois

Reviews

React Careers Values Section, Grid Layout + Framer Motion