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 →
100% fair-trade, ethically sourced shea butter.
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
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;
  }
}

May 11, 2026

SECTIONS

Dual Push Cards

Two-up CTA card section with scroll-driven parallax on each card's background image. Cards scale down and drift vertically as you scroll past. Glassmorphic blur buttons at bottom-left. Stacks on mobile, side-by-side grid on desktop.

May 4, 2026

SECTIONS

Portfolio Grid

A responsive portfolio showcase section with a header tagline, blinking cursor counters, a 2-up/4-col project card grid with hover-zoom images and data-label metadata, plus a full-width CTA button. Scroll-triggered fade and move-up animations via GSAP.

May 1, 2026

SECTIONS

Logo Wall Cycle

A responsive logo grid that cycles through brand logos with smooth GSAP-powered swap animations. Shows 8 logos on desktop and 6 on tablet, shuffling hidden logos into view on a timed loop. Pauses when out of viewport or tab is hidden.