Volver al blog

Crea un portafolio de desarrollador con secciones de React

Publicado el 20 de marzo de 2026·7 min read

Un portafolio de desarrollador es la señal más directa de lo que eres capaz de construir. Los reclutadores dedican una media de 6 segundos a un portafolio antes de decidir si profundizan o pasan de largo. Eso significa que cada sección tiene que justificar su sitio: jerarquía clara, tiempos de carga rápidos y cero relleno. Esta guía recorre la creación de un portafolio de desarrollador completo en React con secciones listas para usar, desde el hero hasta el formulario de contacto.

El conjunto de secciones

Un portafolio de desarrollador que convierte sigue una estructura predecible. Cada sección tiene una única función:

  1. Hero: quién eres, a qué te dedicas, un solo CTA
  2. Vitrina de proyectos: tus 3 a 6 mejores proyectos en una cuadrícula o layout bento
  3. Sobre mí / Skills: trayectoria, stack tecnológico, personalidad
  4. Timeline: experiencia laboral y/o formación, en orden cronológico inverso
  5. Contacto: formulario o enlaces directos, sin fricción

Son cinco secciones. Ni diez ni dos. Con cinco secciones cubres todo lo que un responsable de contratación o un cliente potencial necesita ver. Vamos a construir cada una.

1. Hero con nombre y título

El hero de un portafolio es más sencillo que el de un SaaS. Sin barra de prueba social, sin CTA secundario, sin badge. Solo tu nombre, tu título y una única llamada a la acción:

interface PortfolioHeroProps {
  name: string;
  title: string;
  description: string;
  ctaLabel: string;
  ctaUrl: string;
  avatarUrl?: string;
}

export default function PortfolioHero({ name, title, description, ctaLabel, ctaUrl, avatarUrl }: PortfolioHeroProps) {
  return (
    <section
      style={{
        minHeight: "90vh",
        display: "flex",
        alignItems: "center",
        padding: "var(--section-padding-y-lg) 0",
        background: "var(--color-background)",
      }}
    >
      <div style={{
        maxWidth: "var(--container-max-width)",
        margin: "0 auto",
        padding: "0 var(--container-padding-x)",
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        textAlign: "center",
        gap: "1.5rem",
      }}>
        {avatarUrl && (
          <img
            src={avatarUrl}
            alt={name}
            style={{ width: 96, height: 96, borderRadius: "50%", objectFit: "cover" }}
          />
        )}
        <h1 style={{
          fontSize: "clamp(2.5rem, 5vw, 4rem)",
          fontWeight: 700,
          lineHeight: 1.1,
          letterSpacing: "-0.03em",
          color: "var(--color-foreground)",
        }}>
          {name}
        </h1>
        <p style={{
          fontSize: "1.25rem",
          color: "var(--color-accent)",
          fontWeight: 500,
        }}>
          {title}
        </p>
        <p style={{
          fontSize: "1.0625rem",
          lineHeight: 1.7,
          color: "var(--color-foreground-muted)",
          maxWidth: "560px",
        }}>
          {description}
        </p>
        <a
          href={ctaUrl}
          style={{
            display: "inline-flex",
            alignItems: "center",
            gap: "8px",
            padding: "0.875rem 2rem",
            borderRadius: "var(--radius-full)",
            background: "var(--color-accent)",
            color: "#fff",
            fontWeight: 600,
            fontSize: "0.9375rem",
            textDecoration: "none",
          }}
        >
          {ctaLabel}
        </a>
      </div>
    </section>
  );
}

El CTA debería enlazar a tu sección de proyectos (#projects) o directamente a tu formulario de contacto (#contact), según tu objetivo. Si estás buscando trabajo, un "Ver mi trabajo" que apunte a los proyectos es la opción más potente.

2. Cuadrícula de la vitrina de proyectos

La cuadrícula de proyectos es la sección más importante de tu portafolio. Usa un CSS Grid responsive con 2 columnas en escritorio y 1 en móvil:

interface Project {
  title: string;
  description: string;
  tags: string[];
  imageUrl: string;
  liveUrl?: string;
  repoUrl?: string;
}

export function ProjectGrid({ projects }: { projects: Project[] }) {
  return (
    <section id="projects" style={{ padding: "var(--section-padding-y) 0" }}>
      <div style={{
        maxWidth: "var(--container-max-width)",
        margin: "0 auto",
        padding: "0 var(--container-padding-x)",
      }}>
        <h2 style={{ fontSize: "2rem", fontWeight: 700, marginBottom: "3rem" }}>
          Selected Work
        </h2>
        <div style={{
          display: "grid",
          gridTemplateColumns: "repeat(auto-fit, minmax(min(100%, 400px), 1fr))",
          gap: "2rem",
        }}>
          {projects.map((project) => (
            <article
              key={project.title}
              style={{
                borderRadius: "var(--radius-lg)",
                border: "1px solid var(--color-border)",
                overflow: "hidden",
                background: "var(--color-surface)",
              }}
            >
              <img
                src={project.imageUrl}
                alt={project.title}
                style={{ width: "100%", height: 240, objectFit: "cover" }}
              />
              <div style={{ padding: "1.5rem" }}>
                <h3 style={{ fontSize: "1.25rem", fontWeight: 600 }}>{project.title}</h3>
                <p style={{
                  fontSize: "0.9375rem",
                  color: "var(--color-foreground-muted)",
                  lineHeight: 1.6,
                  marginTop: "0.5rem",
                }}>
                  {project.description}
                </p>
                <div style={{ display: "flex", flexWrap: "wrap", gap: "0.5rem", marginTop: "1rem" }}>
                  {project.tags.map((tag) => (
                    <span
                      key={tag}
                      style={{
                        padding: "0.25rem 0.75rem",
                        borderRadius: "var(--radius-full)",
                        background: "var(--color-accent-subtle)",
                        color: "var(--color-foreground-muted)",
                        fontSize: "0.8125rem",
                      }}
                    >
                      {tag}
                    </span>
                  ))}
                </div>
              </div>
            </article>
          ))}
        </div>
      </div>
    </section>
  );
}

Selecciona sin piedad. Tres proyectos excelentes ganan a diez mediocres. Cada tarjeta de proyecto debería incluir: una captura de pantalla (no un logo), una descripción de una frase sobre qué construiste y por qué, y las tech tags. Enlaza al sitio en producción siempre que puedas: los reclutadores quieren hacer clic y verlo funcionando.

3. Sección Sobre mí y Skills

La sección sobre mí te humaniza más allá de una lista de proyectos. Mantenla concisa, dos párrafos cortos como máximo, y acompáñala de una cuadrícula de skills:

<div style={{
  display: "grid",
  gridTemplateColumns: "repeat(auto-fit, minmax(120px, 1fr))",
  gap: "1rem",
  marginTop: "2rem",
}}>
  {skills.map((skill) => (
    <div
      key={skill.name}
      style={{
        display: "flex",
        flexDirection: "column",
        alignItems: "center",
        gap: "0.5rem",
        padding: "1.25rem",
        borderRadius: "var(--radius-md)",
        border: "1px solid var(--color-border)",
      }}
    >
      {skill.icon}
      <span style={{ fontSize: "0.8125rem", fontWeight: 500 }}>{skill.name}</span>
    </div>
  ))}
</div>

Muestra entre 8 y 12 skills como máximo. Si listas 30 tecnologías, ninguna parece un punto fuerte. Agrúpalas de forma lógica: lenguajes, frameworks, herramientas. Usa iconos reconocibles (de lucide-react o react-icons) en lugar de texto plano, porque el ojo escanea más rápido lo visual.

4. Sección Timeline

Un timeline cronológico inverso de tu experiencia laboral le da a los reclutadores los datos estructurados que están buscando. El patrón visual es una línea vertical con nodos:

interface TimelineItem {
  date: string;
  title: string;
  company: string;
  description: string;
}

export function Timeline({ items }: { items: TimelineItem[] }) {
  return (
    <div style={{ position: "relative", paddingLeft: "2rem" }}>
      {/* Vertical line */}
      <div
        aria-hidden
        style={{
          position: "absolute",
          left: "7px",
          top: 0,
          bottom: 0,
          width: "2px",
          background: "var(--color-border)",
        }}
      />
      {items.map((item) => (
        <div key={`${item.company}-${item.date}`} style={{ position: "relative", paddingBottom: "2.5rem" }}>
          {/* Node dot */}
          <div
            aria-hidden
            style={{
              position: "absolute",
              left: "-2rem",
              top: "4px",
              width: "16px",
              height: "16px",
              borderRadius: "50%",
              background: "var(--color-accent)",
              border: "3px solid var(--color-background)",
            }}
          />
          <span style={{ fontSize: "0.8125rem", color: "var(--color-foreground-muted)" }}>
            {item.date}
          </span>
          <h3 style={{ fontSize: "1.125rem", fontWeight: 600, marginTop: "0.25rem" }}>
            {item.title}
          </h3>
          <p style={{ fontSize: "0.9375rem", color: "var(--color-accent)", fontWeight: 500 }}>
            {item.company}
          </p>
          <p style={{
            fontSize: "0.9375rem",
            color: "var(--color-foreground-muted)",
            lineHeight: 1.6,
            marginTop: "0.5rem",
          }}>
            {item.description}
          </p>
        </div>
      ))}
    </div>
  );
}

Limita cada descripción a 1 o 2 frases. Pon el foco en el impacto ("Reduje el tiempo de carga un 40 %") por encima de las responsabilidades ("Responsable del desarrollo frontend").

5. Formulario de contacto

La última sección elimina la fricción. Un formulario sencillo con campos de nombre, email y mensaje es suficiente. No pidas número de teléfono, tamaño de empresa ni presupuesto, porque esto no es un formulario de captación de leads de un SaaS:

<form
  action="/api/contact"
  method="POST"
  style={{ display: "flex", flexDirection: "column", gap: "1rem", maxWidth: "480px" }}
>
  <input
    name="name"
    placeholder="Name"
    required
    style={{
      padding: "0.75rem 1rem",
      borderRadius: "var(--radius-md)",
      border: "1px solid var(--color-border)",
      background: "var(--color-surface)",
      color: "var(--color-foreground)",
      fontSize: "0.9375rem",
    }}
  />
  <input name="email" type="email" placeholder="Email" required style={{ /* same styles */ }} />
  <textarea
    name="message"
    placeholder="Message"
    rows={5}
    required
    style={{ /* same styles, resize: "vertical" */ }}
  />
  <button
    type="submit"
    style={{
      padding: "0.875rem 2rem",
      borderRadius: "var(--radius-full)",
      background: "var(--color-accent)",
      color: "#fff",
      fontWeight: 600,
      border: "none",
      cursor: "pointer",
    }}
  >
    Send Message
  </button>
</form>

Añade enlaces sociales (GitHub, LinkedIn, Twitter/X) junto al formulario o debajo. Hay visitantes que prefieren escribirte por DM antes que rellenar un formulario.

Montar el portafolio completo

Cada sección anterior es independiente y autónoma. Colócalas dentro de un único componente de página en este orden: Hero, Proyectos, Sobre mí, Timeline, Contacto. Añade una navbar fija con enlaces de ancla (#projects, #about, #experience, #contact) y un footer, y ya tienes un portafolio completo.

En lugar de construir cada sección desde cero, echa un vistazo al catálogo de heros de Incubator para variantes de hero pulidas, al catálogo de portafolios para layouts de vitrina de proyectos, al catálogo de secciones sobre mí para secciones de bio y skills, y al catálogo de contacto para diseños de formulario. Si quieres un portafolio completo montado a partir de secciones listas para usar, el generador de páginas de portafolio te permite elegir secciones y exportar una página entera. Cada sección viene preparada para TypeScript, Tailwind CSS v4 y Next.js 15: pega, personaliza tu contenido y despliega.

Relacionado en incubator

VA

Victor Aubague

Desarrollador y creador de Incubator

Desarrollador full-stack especializado en React, Next.js y TypeScript. Creé Incubator para ayudar a los desarrolladores a lanzar interfaces bonitas más rápido, todos los componentes provienen de proyectos reales de clientes y de código en producción.

LinkedIn

Related articles

Crea un portafolio de desarrollador con secciones de React