Retour au catalogue

About Split Photo

Grande photo a gauche avec texte mission/vision a droite. Layout split classique.

aboutsimple Both Responsive a11y
elegantcorporateuniversalagencyportfoliosplit
Theme

How to build a split photo about section in React

A split photo about section in React places a tall portrait image on the left and labeled mission/vision copy on the right inside a two-column CSS grid. Framer Motion's whileInView triggers an opacity + horizontal slide for each column as the section enters the viewport.

  • Stack: React + Framer Motion + lucide-react, ~173 lines, zero extra dependencies.
  • Animation: whileInView with once:true, 0.6s duration, custom cubic-bezier ease [0.16, 1, 0.3, 1], 0.1s stagger between columns.
  • Image block: fixed 4/5 aspect ratio, rounded via CSS token, accent-colored background as a placeholder.
  • Accessible: semantic h2 + h3 hierarchy, standard anchor for the CTA, no ARIA hacks needed.
  • Responsive caveat: the two-column grid is set inline; add a media query or Tailwind breakpoint to stack columns on mobile.

About Split Photo is a React section that pairs a large portrait image with concise mission and vision copy in a two-column layout. It anchors the credibility of a brand or team above the fold of any about page, making the visual do half the storytelling work while the text stays scannable.

Anatomy

A full-width section wraps a centered container capped at `--container-max-width`. Inside, a CSS grid with `1fr 1fr` columns and a 4rem gap holds two animated blocks. The left block is a motion.div with a fixed 4/5 aspect ratio, rounded corners via `--radius-lg`, and an accent-colored background acting as an image placeholder. The right block stacks an h2 with a serif italic accent, two labeled subsections (Mission, Vision) with uppercase tracking headings, and a pill-shaped CTA link with an ArrowRight icon.

How it works

Each column is a `motion.div` that starts offset: the photo starts at `{ opacity: 0, x: -24 }` and the text at `{ opacity: 0, x: 24 }`. Both animate to `{ opacity: 1, x: 0 }` when they enter the viewport (`whileInView`, `viewport={{ once: true }}`). The custom cubic-bezier ease `[0.16, 1, 0.3, 1]` gives the motion an aggressive start and a soft landing, so the columns appear to snap into place. A 0.1s delay on the text column creates a stagger effect without a third-party orchestration utility.

How to build it in React

  1. Set up the two-column grid

    Wrap the section content in a div with `display: grid`, `gridTemplateColumns: '1fr 1fr'`, `gap: '4rem'`, and `alignItems: 'center'`. Use CSS tokens for padding and max-width so the layout adapts to every theme preset without extra overrides.

    <div style={{
      display: "grid",
      gridTemplateColumns: "1fr 1fr",
      gap: "4rem",
      alignItems: "center",
      maxWidth: "var(--container-max-width)",
      padding: "0 var(--container-padding-x)",
      margin: "0 auto",
    }}>
  2. Animate the photo column into view

    Replace the photo div with a `motion.div`. Set `initial={{ opacity: 0, x: -24 }}` and `whileInView={{ opacity: 1, x: 0 }}` with `viewport={{ once: true }}`. Use the custom ease array for a snappy, natural feel. Fix the aspect ratio to 4/5 so the block stays proportional at any column width.

    const EASE = [0.16, 1, 0.3, 1] as const;
    
    <motion.div
      initial={{ opacity: 0, x: -24 }}
      whileInView={{ opacity: 1, x: 0 }}
      viewport={{ once: true }}
      transition={{ duration: 0.6, ease: EASE }}
      style={{ aspectRatio: "4/5", borderRadius: "var(--radius-lg)" }}
    />
  3. Stagger the text column

    Wrap the text content in a second `motion.div` with `initial={{ opacity: 0, x: 24 }}` (slides from the right) and a `delay: 0.1` in the transition. The 100ms offset is enough to read as a deliberate stagger without making the user wait.

    <motion.div
      initial={{ opacity: 0, x: 24 }}
      whileInView={{ opacity: 1, x: 0 }}
      viewport={{ once: true }}
      transition={{ duration: 0.6, delay: 0.1, ease: EASE }}
    />
  4. Structure the mission/vision copy

    Give each subsection an h3 with uppercase tracking (font-size 0.75rem, letter-spacing 0.1em, accent color) as a label, then a paragraph for the body copy. Keep the h2 above them and use a serif italic span for the accent part of the title. Finish with a pill CTA link.

When to use it

Reach for this layout on agency, studio, or SaaS about pages where a strong visual anchors the brand story. It works equally well mid-page as an interlude between feature blocks and at the top of a dedicated /about route. Skip it when the image is unavailable or purely decorative, a text-only about section avoids the placeholder awkwardness. On mobile, always stack the columns vertically so the photo does not get crushed to a narrow strip.

Used by

  • Stripe, Uses a large photo/text split on its About page to anchor leadership and company culture.
  • Notion, Pairs editorial photography with mission-driven copy in a two-column layout across its about page.
  • Loom, Split layout with team photo left, values copy right, a direct analog of this pattern.

FAQ

How do I make the two columns stack on mobile?

The grid is set via inline styles, so add a CSS media query or a Tailwind responsive class to switch `gridTemplateColumns` to `1fr` below the `md` breakpoint. A quick approach is to replace the inline style with a Tailwind class like `grid-cols-1 md:grid-cols-2`.

Can I replace the placeholder with an actual image?

Yes. Swap the accent-colored div for a Next.js `<Image>` or a plain `<img>` with `object-fit: cover` and `width: 100%; height: 100%` inside the motion.div. Keep the `aspectRatio: '4/5'` on the container so the layout does not shift.

Why does the animation trigger only once?

`viewport={{ once: true }}` tells Framer Motion to fire whileInView a single time and leave the element in its animated state. Remove that option if you want the columns to reset and re-animate each time the user scrolls past them.

How do I change the CTA style from filled to outlined?

Remove `background: 'var(--color-accent)'` from the link and add `border: '2px solid var(--color-accent)'` plus `color: 'var(--color-accent)'`. Adjust the hover state accordingly via a CSS class or Framer Motion whileHover.

"use client";

import { motion } from "framer-motion";
import { ImageIcon, ArrowRight } from "lucide-react";

interface AboutSplitPhotoProps {
  title?: string;
  titleAccent?: string;
  mission?: string;
  vision?: string;
  ctaLabel?: string;
  ctaUrl?: string;
  imageColor?: string;
}

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

export default function AboutSplitPhoto({
  title = "A propos",
  titleAccent = "de nous",
  mission = "Notre mission.",
  vision = "Notre vision.",

Code complet réservé à Pro

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

Passer en Pro, 9,99€/mois

Reviews

React Split About Section with Photo, Code + Tutorial