Ir al contenido
Evaluar mi proyecto
Componentes · MicrointeraccionesDisponible

Stepper con línea

Pasos verticales conectados por una línea que se va rellenando.

  1. Diagnóstico operativo
    Conversamos sobre tu operación: qué funciona, qué duele, qué quieres cambiar.
  2. Propuesta técnica y comercial
    Alcance acotado, costo y tiempos claros. Apruebas antes de empezar.
  3. Ahora
    Construcción por sprints
    Entregamos en ciclos cortos de 1 a 2 semanas, con demos reales cada semana.
  4. Lanzamiento y estabilización
    Salimos a producción contigo y nos quedamos 30 días para asegurar el cambio.
  5. Acompañamiento
    Plan mensual de mantenimiento o CTO externo, según tu operación.

Código completo

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

line-stepper.tsx138 líneas
Descargar
"use client";

/**
 * Stepper vertical con línea que se rellena: cada paso lleva número, título y cuerpo opcional.
 * La línea vertical rellena en cuanto el paso anterior se marca como completado; el activo escala
 * y muestra un aura; el pendiente queda tenue. Acepta colores configurables.
 *
 *   <LineStepper
 *     steps={[
 *       { title: "Diagnóstico", body: "Entendemos tu operación." },
 *       { title: "Propuesta",   body: "Alcance, costo y tiempos." }
 *     ]}
 *     current={1}
 *   />
 */

import type { CSSProperties } from "react";

export type LineStep = {
  title: string;
  body?: string;
};

const CSS = `
.glsp-root { list-style: none; margin: 0; padding: 0; font-family: inherit; counter-reset: glsp; }
.glsp-item { position: relative; padding: 0 0 28px 56px; transition: opacity .35s ease; }
.glsp-item:last-child { padding-bottom: 0; }
.glsp-item[data-state="pending"] .glsp-body { opacity: .42; }
.glsp-dot { position: absolute; left: 0; top: 4px; width: 36px; height: 36px; border-radius: 50%; background: var(--glsp-paper, #fff); border: 1.5px solid var(--glsp-rail); display: grid; place-items: center; font-size: 13px; font-weight: 700; color: var(--glsp-muted); transition: transform .42s cubic-bezier(.34, 1.36, .64, 1), background-color .35s ease, border-color .35s ease, color .35s ease, box-shadow .35s ease; }
.glsp-item[data-state="active"] .glsp-dot { border-color: var(--glsp-accent); color: var(--glsp-accent); transform: scale(1.08); box-shadow: 0 0 0 6px var(--glsp-glow); }
.glsp-item[data-state="done"] .glsp-dot { background: var(--glsp-accent); border-color: var(--glsp-accent); color: var(--glsp-paper, #fff); }
.glsp-num { display: block; line-height: 1; }
.glsp-check { display: none; }
.glsp-item[data-state="done"] .glsp-num { display: none; }
.glsp-item[data-state="done"] .glsp-check { display: block; line-height: 1; }
.glsp-line { position: absolute; left: 17px; top: 38px; bottom: -8px; width: 2px; background: var(--glsp-rail); border-radius: 2px; overflow: hidden; }
.glsp-line::after { content: ""; position: absolute; left: 0; right: 0; top: 0; height: 100%; background: var(--glsp-accent); transform: scaleY(0); transform-origin: top; transition: transform .55s cubic-bezier(.7, 0, .3, 1); }
.glsp-item[data-state="done"] + .glsp-item .glsp-line::after,
.glsp-item[data-state="done"] .glsp-line::after { transform: scaleY(1); }
.glsp-item[data-state="active"] .glsp-line::after { transform: scaleY(.5); }
.glsp-body { transition: opacity .35s ease; }
.glsp-title { font-size: 17px; font-weight: 600; line-height: 1.3; letter-spacing: -.015em; color: var(--glsp-ink); }
.glsp-text { margin-top: 4px; font-size: 14px; line-height: 1.55; color: var(--glsp-muted); max-width: 56ch; }
.glsp-tag { display: inline-block; font-size: 11px; font-weight: 600; letter-spacing: .14em; text-transform: uppercase; padding: 3px 8px; border-radius: 4px; background: var(--glsp-glow); color: var(--glsp-accent); margin-bottom: 6px; }
@keyframes glsp-pulse { 0%, 100% { box-shadow: 0 0 0 6px var(--glsp-glow); } 50% { box-shadow: 0 0 0 10px var(--glsp-glow); } }
.glsp-item[data-state="active"] .glsp-dot { animation: glsp-pulse 2s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
  .glsp-item[data-state="active"] .glsp-dot { animation: none; transform: scale(1.08); }
  .glsp-dot, .glsp-line::after { transition: none; }
}
`;

function markState(index: number, current: number) {
  if (index < current) return "done";
  if (index === current) return "active";
  return "pending";
}

export function LineStepper({
  steps,
  current,
  accent,
  ink,
  muted,
  rail,
  paper,
  className,
  style,
}: {
  steps: LineStep[];
  current: number;
  accent?: string;
  ink?: string;
  muted?: string;
  rail?: string;
  paper?: string;
  className?: string;
  style?: CSSProperties;
}) {
  const accentCss = accent ?? "#0b81ff";
  const inkCss = ink ?? "#0e0e0f";
  const mutedCss = muted ?? "#545456";
  const railCss = rail ?? "rgba(14, 14, 15, .12)";
  const paperCss = paper ?? "#ffffff";

  const vars = {
    "--glsp-accent": accentCss,
    "--glsp-ink": inkCss,
    "--glsp-muted": mutedCss,
    "--glsp-rail": railCss,
    "--glsp-paper": paperCss,
    "--glsp-glow": `${accentCss}26`,
  } as CSSProperties;

  return (
    <>
      <style>{CSS}</style>
      <ol className={["glsp-root", className].filter(Boolean).join(" ")} style={{ ...vars, ...style }}>
        {steps.map((step, index) => {
          const state = markState(index, current);
          const num = String(index + 1).padStart(2, "0");
          return (
            <li key={`${num}-${step.title}`} className="glsp-item" data-state={state}>
              <span className="glsp-dot" aria-hidden="true">
                <span className="glsp-num">{num}</span>
                <svg className="glsp-check" width="14" height="14" viewBox="0 0 14 14" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round">
                  <path d="M3 7.5l3 3L11 4" />
                </svg>
              </span>
              <span className="glsp-line" aria-hidden="true" />
              <div className="glsp-body">
                {state === "active" ? <span className="glsp-tag">Ahora</span> : null}
                <div className="glsp-title">{step.title}</div>
                {step.body ? <div className="glsp-text">{step.body}</div> : null}
              </div>
            </li>
          );
        })}
      </ol>
    </>
  );
}

export function LineStepperDemo() {
  const steps: LineStep[] = [
    { title: "Diagnóstico operativo", body: "Conversamos sobre tu operación: qué funciona, qué duele, qué quieres cambiar." },
    { title: "Propuesta técnica y comercial", body: "Alcance acotado, costo y tiempos claros. Apruebas antes de empezar." },
    { title: "Construcción por sprints", body: "Entregamos en ciclos cortos de 1 a 2 semanas, con demos reales cada semana." },
    { title: "Lanzamiento y estabilización", body: "Salimos a producción contigo y nos quedamos 30 días para asegurar el cambio." },
    { title: "Acompañamiento", body: "Plan mensual de mantenimiento o CTO externo, según tu operación." },
  ];
  return (
    <div style={{ background: "#f7f8fb", padding: 28, borderRadius: 14, maxWidth: 460 }}>
      <LineStepper steps={steps} current={2} />
    </div>
  );
}

Úsalo en tu proyecto

Instálalo con la CLI de shadcn.

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

Ejemplo de uso de Stepper con línea

Después de copiar line-stepper.tsx a tu proyecto, impórtalo y colócalo donde lo necesites. Exporta 2 componentes: LineStepper, LineStepperDemo.

ejemplo.tsx5 líneas
import { LineStepper } from "./line-stepper";

export default function Ejemplo() {
  return <LineStepper />;
}

Personaliza Stepper con línea 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
--glsp-paper#fff

Accesibilidad y rendimiento

El archivo tiene 138 líneas (6 KB) y define 1 animación con @keyframes. Esto es lo que hace Stepper con línea 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-*.
  • Es un componente de cliente de Next.js: incluye la directiva "use client".
Licencia
MIT
Dependencias
React >=18
Compatibilidad
React · Next.js (quita la directiva use client fuera de Next.js) · Sin Tailwind

Acerca del producto

Un stepper vertical donde cada paso lleva un círculo con un número y un título. La línea entre los pasos se rellena en cuanto el paso anterior se completa, el activo escala suavemente y el pendiente queda tenue. Acepta un color de acento.

Incluye

Un solo archivo React + TypeScript, con los estilos dentro. No usa Tailwind ni bibliotecas externas.

  • Línea entre pasos que rellena según el progreso
  • Estado activo con scale y aura
  • Estado completado con marca de verificación
  • Acento configurable, paleta por defecto
  • Cumple prefers-reduced-motion

Cómo usarlo

Pasa steps como un arreglo de { title, body? } y current como índice del paso activo. accent, ink y rail son opcionales.