SectionsSimpleApril 13, 2026
About Section
A two-column about section with a full-bleed image on one side and centered text on the other. Supports reversible layout and mobile text truncation with a read-more toggle.
View Full Demo →Preview

The Source
100% fair-trade, ethically sourced shea butter.
Every jar of SheaBinta starts in West Africa, where women-led cooperatives produce unrefined shea using traditional methods. They're paid fairly. Their communities are supported.
The butter they make is the real thing — unprocessed, nutrient-rich, and nothing like what you'll find in most "shea" products on the shelf.
Our Journey →Source
demo.jsx
import AboutSection from "./index.jsx";
import { aboutSection } from "../demo-data.js";
export default function AboutSectionDemo() {
return <AboutSection {...aboutSection} />;
}
index.jsx
"use client";
import { useState, useEffect } from "react";
import styles from "./styles.module.css";
function RichText({ text }) {
// Parses **bold** markers into <strong> tags
const parts = text.split(/(\*\*[^*]+\*\*)/g);
return parts.map((part, i) => {
if (part.startsWith("**") && part.endsWith("**")) {
return <strong key={i}>{part.slice(2, -2)}</strong>;
}
return part;
});
}
export default function AboutSection({
label,
title,
headingEmphasis,
content = [],
image,
imageCaption,
reverse = false,
ctaLabel,
ctaHref = "#",
charLimit = 509,
}) {
const [isExpanded, setIsExpanded] = useState(false);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth < 768);
handleResize();
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}, []);
const fullText = content.join(" ");
const shouldTruncate = isMobile && !isExpanded && fullText.length > charLimit;
const displayText = shouldTruncate
? fullText.slice(0, charLimit) + "..."
: content.join("\n\n");
const paragraphs = displayText.split("\n\n");
const renderHeading = () => {
if (!headingEmphasis || !title) return title;
const parts = title.split(headingEmphasis);
if (parts.length < 2) return title;
return (
<>
{parts[0]}<em>{headingEmphasis}</em>{parts[1]}
</>
);
};
return (
<section className={`${styles.section} ${reverse ? styles.reverse : ""}`}>
<div className={styles.wrapper}>
<div className={styles.imageCol}>
<img src={image} alt={title} className={styles.image} />
{imageCaption && (
<span className={styles.imageCaption}>{imageCaption}</span>
)}
</div>
<div className={styles.text}>
{label && <span className={styles.label}>{label}</span>}
<h2 className={styles.heading}>{renderHeading()}</h2>
{paragraphs.map((para, i) => (
<p key={i} className={styles.paragraph}>
<RichText text={para} />
</p>
))}
{isMobile && fullText.length > charLimit && (
<button
className={styles.readMore}
onClick={() => setIsExpanded((prev) => !prev)}
>
{isExpanded ? "Read Less" : "Read More"}
</button>
)}
{ctaLabel && (
<a href={ctaHref} className={styles.cta}>
{ctaLabel} →
</a>
)}
</div>
</div>
</section>
);
}
styles.module.css
.section {
display: flex;
flex-direction: column;
width: 100%;
min-height: 70vh;
box-sizing: border-box;
overflow: hidden;
background: #f1ebe7;
}
/* ---- Two-column wrapper ---- */
.wrapper {
display: flex;
flex-direction: column;
width: 100%;
flex: 1;
max-width: 1400px;
margin: 0 auto;
padding: 6rem 3rem;
gap: 3rem;
}
/* ---- Image column ---- */
.imageCol {
position: relative;
width: 100%;
overflow: hidden;
}
.image {
width: 100%;
height: auto;
display: block;
object-fit: cover;
}
/* ---- Image caption ---- */
.imageCaption {
position: absolute;
bottom: 1.5rem;
left: 1.5rem;
right: 1.5rem;
font-size: 0.55rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: rgba(0, 0, 0, 0.5);
background: rgba(255, 255, 255, 0.7);
padding: 0.6rem 1rem;
text-align: center;
line-height: 1.5;
}
/* ---- Text column ---- */
.text {
width: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: flex-start;
text-align: left;
gap: 0.5rem;
}
/* ---- Typography ---- */
.label {
font-size: 0.65rem;
font-weight: 500;
color: #000;
text-transform: uppercase;
letter-spacing: 0.12em;
margin-bottom: 0.25rem;
}
.heading {
font-family: Georgia, "Times New Roman", serif;
font-size: clamp(2.25rem, 4vw, 3.5rem);
font-weight: 400;
line-height: 1.1;
letter-spacing: -0.02em;
color: #1a1a1a;
max-width: 18ch;
margin-bottom: 0.5rem;
}
.heading em {
font-style: italic;
}
.paragraph {
font-size: clamp(14px, 1.1vw, 16px);
line-height: 1.6;
color: #555;
max-width: 42ch;
}
.paragraph strong {
color: #1a1a1a;
font-weight: 600;
}
/* ---- CTA link ---- */
.cta {
display: inline-block;
font-size: 0.65rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #1a1a1a;
text-decoration: none;
border-bottom: 1px solid #1a1a1a;
padding-bottom: 0.2rem;
margin-top: 1rem;
transition: color 0.3s ease, border-color 0.3s ease;
}
.cta:hover {
color: #555;
border-color: #555;
}
/* ---- Read more ---- */
.readMore {
background: none;
border: none;
font-size: 0.65rem;
font-weight: 500;
color: #333;
text-transform: uppercase;
letter-spacing: 0.08em;
text-decoration: underline;
cursor: pointer;
margin-top: 0.5rem;
padding: 0;
}
/* ---- Desktop ---- */
@media (min-width: 769px) {
.wrapper {
flex-direction: row;
align-items: center;
padding: 6rem 5rem;
gap: 5rem;
}
.reverse .wrapper {
flex-direction: row-reverse;
}
.imageCol {
width: 50%;
flex-shrink: 0;
}
.text {
width: 50%;
}
}
/* ---- Mobile ---- */
@media (max-width: 768px) {
.wrapper {
padding: 3rem 1.5rem;
}
.section {
min-height: auto;
}
}