Ir al contenido
Evaluar mi proyecto
Componentes · SeccionesDisponible

Proceso en cuatro etapas

Una línea con iconos se traza al entrar en pantalla y cada etapa se ilumina en secuencia. Editorial.

Cómo trabajamos, paso a paso

  1. Etapa 1 de 4

    Escuchamos

    Entendemos cómo trabajas hoy y qué te frena.

  2. Etapa 2 de 4

    Proponemos

    Alcance, calendario y precio claros, por escrito.

  3. Etapa 3 de 4

    Construimos

    Sprints cortos con entregas que puedes probar.

  4. Etapa 4 de 4

    Acompañamos

    Lanzamiento, capacitación y soporte continuo.

Demostración interactiva

Código completo

Es exactamente el archivo que se descarga. Cópialo a tu proyecto y úsalo.

seccion-proceso.tsx99 líneas
Descargar
"use client";

import { useEffect, useRef, useState, useSyncExternalStore, type CSSProperties, type ReactNode } from "react";

/** Proceso en cuatro etapas: una línea con iconos dibujados se traza al entrar en pantalla y cada etapa se ilumina en secuencia. El orden importa, por eso las etapas van numeradas. Estilo editorial. React + TypeScript, sin dependencias. */
export type SeccionProcesoEtapa = { title: string; text: string; icon: ReactNode };

export type SeccionProcesoProps = {
  title?: string;
  steps?: SeccionProcesoEtapa[];
  className?: string;
  style?: CSSProperties;
};

const CSS = `
.gcspr-root { box-sizing: border-box; container-type: inline-size; width: 100%; padding: clamp(32px, 6cqi, 88px) clamp(18px, 5cqi, 72px); background: #f1ede4; color: #17150f; font: 400 15px/1.55 ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif; }
.gcspr-root *, .gcspr-root *::before, .gcspr-root *::after { box-sizing: border-box; }
@media (prefers-reduced-motion: reduce) { .gcspr-root * { transition-duration: .01ms !important; } }
.gcspr-title { max-width: 16ch; margin: 0 0 clamp(28px, 5cqi, 56px); font: 500 clamp(30px, 5cqi, 60px)/1.04 "Newsreader", Georgia, ui-serif, serif; letter-spacing: -.03em; text-wrap: balance; }
.gcspr-grid { position: relative; display: grid; grid-template-columns: repeat(var(--n), minmax(0, 1fr)); gap: 24px; margin: 0; padding: 0; list-style: none; }
.gcspr-line { position: absolute; top: 34px; left: 34px; right: 34px; height: 2px; background: #d6d0c0; }
.gcspr-line::after { content: ""; position: absolute; inset: 0; background: #17150f; transform: scaleX(var(--p, 0)); transform-origin: left; transition: transform 1.6s cubic-bezier(.65, 0, .35, 1); }
.gcspr-step { position: relative; display: grid; gap: 14px; align-content: start; }
.gcspr-ico { display: grid; place-items: center; width: 68px; height: 68px; border: 2px solid #d6d0c0; border-radius: 50%; background: #f1ede4; color: #a69f8b; transition: border-color .5s, color .5s, background .5s, transform .6s cubic-bezier(.34, 1.56, .64, 1); }
.gcspr-step[data-on="true"] .gcspr-ico { border-color: #17150f; color: #fff; background: #17150f; transform: scale(1.04); }
.gcspr-ico svg { width: 28px; height: 28px; }
.gcspr-n { font: 500 13px/1 ui-monospace, "SF Mono", Menlo, monospace; letter-spacing: .08em; color: #8c8573; }
.gcspr-step h3 { margin: 0; font: 500 24px/1.1 "Newsreader", Georgia, ui-serif, serif; letter-spacing: -.01em; }
.gcspr-step p { margin: 0; color: #5f594a; max-width: 28ch; }
@container (max-width: 760px) { .gcspr-grid { grid-template-columns: minmax(0, 1fr); gap: 28px; } .gcspr-line { top: 34px; bottom: 34px; left: 34px; right: auto; width: 2px; height: auto; } .gcspr-line::after { transform: scaleY(var(--p, 0)); transform-origin: top; } .gcspr-step { grid-template-columns: 68px 1fr; column-gap: 16px; row-gap: 4px; align-content: center; } .gcspr-step .gcspr-ico { grid-row: span 2; } .gcspr-step .gcspr-n { grid-column: 2; align-self: end; } .gcspr-step > div { grid-column: 2; } }`;

const REDUCED_QUERY = "(prefers-reduced-motion: reduce)";
function useReduced() {
  return useSyncExternalStore(
    (notify) => {
      const query = window.matchMedia(REDUCED_QUERY);
      query.addEventListener("change", notify);
      return () => query.removeEventListener("change", notify);
    },
    () => window.matchMedia(REDUCED_QUERY).matches,
    () => false,
  );
}


const I = ({ d }: { d: string }) => (
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
    <path d={d} />
  </svg>
);

const DEFAULT: SeccionProcesoEtapa[] = [
  { title: "Escuchamos", text: "Entendemos cómo trabajas hoy y qué te frena.", icon: <I d="M4 5h16v11H9l-5 4z" /> },
  { title: "Proponemos", text: "Alcance, calendario y precio claros, por escrito.", icon: <I d="M6 3h9l4 4v14H6zM14 3v5h5M9 13h6M9 17h6" /> },
  { title: "Construimos", text: "Sprints cortos con entregas que puedes probar.", icon: <I d="m8 8-4 4 4 4M16 8l4 4-4 4M13.500 5l-3 14" /> },
  { title: "Acompañamos", text: "Lanzamiento, capacitación y soporte continuo.", icon: <I d="M12 21s-7-4.500-9-9a5 5 0 0 1 9-3 5 5 0 0 1 9 3c-2 4.500-9 9-9 9z" /> },
];

export function SeccionProceso({ title = "Cómo trabajamos, paso a paso", steps = DEFAULT, className = "", style }: SeccionProcesoProps) {
  const ref = useRef<HTMLOListElement>(null);
  const reduced = useReduced();
  const [active0, setActive] = useState(0);
  const [run, setRun] = useState(false);
  const active = reduced ? steps.length : active0;

  useEffect(() => {
    const el = ref.current;
    if (!el) return;
    const io = new IntersectionObserver(([entry]) => entry.isIntersecting && (setRun(true), io.disconnect()), { threshold: 0.35 });
    io.observe(el);
    return () => io.disconnect();
  }, []);
  useEffect(() => {
    if (!run || reduced) return;
    const ids = steps.map((_, i) => window.setTimeout(() => setActive(i + 1), 400 + i * 520));
    return () => ids.forEach((id) => window.clearTimeout(id));
  }, [run, steps, reduced]);

  return (
    <section className={`gcspr-root ${className}`} style={style}>
      <style>{CSS}</style>
      <h2 className="gcspr-title">{title}</h2>
      <ol ref={ref} className="gcspr-grid" style={{ "--n": steps.length } as CSSProperties}>
        <span className="gcspr-line" aria-hidden="true" style={{ "--p": active > 0 ? (active - 1) / (steps.length - 1) : 0 } as CSSProperties} />
        {steps.map((step, i) => (
          <li key={step.title} className="gcspr-step" data-on={active > i || !run}>
            <span className="gcspr-ico">{step.icon}</span>
            <span className="gcspr-n">Etapa {i + 1} de {steps.length}</span>
            <div>
              <h3>{step.title}</h3>
              <p>{step.text}</p>
            </div>
          </li>
        ))}
      </ol>
    </section>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

terminal.sh1 líneas
npx shadcn@latest add https://www.gassierconsulting.com/r/seccion-proceso

Ejemplo de uso de Proceso en cuatro etapas

Después de copiar seccion-proceso.tsx a tu proyecto, impórtalo y colócalo donde lo necesites.

ejemplo.tsx5 líneas
import { SeccionProceso } from "./seccion-proceso";

export default function Ejemplo() {
  return <SeccionProceso title="Ejemplo" />;
}

Propiedades de SeccionProceso

SeccionProceso acepta 4 propiedades: todas son opcionales y tienen un valor por defecto, así que funciona sin configurar nada.

PropiedadTipoDescripción
title?stringOpcional.
steps?SeccionProcesoEtapa[]Opcional.
className?stringOpcional.
style?CSSPropertiesOpcional.

Personaliza Proceso en cuatro etapas con variables CSS

Define estas variables en el contenedor o en tu hoja de estilos para cambiar los colores sin tocar el componente. Si no las defines, se usa el valor por defecto.

VariableValor por defecto
--p0

Accesibilidad y rendimiento

El archivo tiene 99 líneas (6 KB) y no necesita @keyframes. Esto es lo que hace Proceso en cuatro etapas por quien lo usa:

  • Detecta prefers-reduced-motion y baja o quita el movimiento cuando la persona lo pide.
  • Expone su estado a tecnologías de apoyo con atributos aria-*.
  • Se pausa cuando sale de la pantalla para no gastar batería ni CPU.
  • Lee preferencias del navegador con useSyncExternalStore, sin parpadeos en la hidratación.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React 18 y 19 · Next.js (componente de cliente)