Projetos

Dê vida ao portfólio: adicionando interações com JavaScript

Aprenda a adicionar interações simples ao portfólio com JavaScript puro, criando menu mobile, fechamento automático, menu com rolagem, revelação de elementos e link ativo por seção.

Por Equipe Central Stack09/07/202610 minGuia prático
portfoliojavascriptdominteraçõesfrontendprojetos práticosiniciantedesenvolvimento web

Compartilhar

Envie este conteúdo para sua rede ou salve o link para publicar depois.

Trilha: Projeto Portfólio Profissional

Este conteúdo é a Parte 3 de 6 da trilha Projeto Portfólio Profissional. Acompanhe o hub da trilha: /projetos/criar-portfolio-profissional

Na Parte 1 criamos a estrutura HTML. Na Parte 2 deixamos o visual mais profissional com CSS. Agora vamos usar JavaScript para adicionar interações simples, úteis e fáceis de entender.

Introdução

JavaScript não precisa aparecer no portfólio como um monte de efeitos confusos. Para um projeto de iniciante, ele deve melhorar a experiência: abrir e fechar menu, atualizar estados visuais, reagir à rolagem e deixar a página mais agradável de usar.

Nesta parte, vamos trabalhar com interações pequenas e práticas. A ideia é que você entenda o papel de cada bloco antes de copiar o código para o seu projeto.

Preparando o arquivo script.js

O arquivo script.js deve ficar separado do HTML e do CSS. Ele será carregado no final do body, depois que o conteúdo da página já existir no navegador.

estrutura
portfolio-profissional/
  index.html
  style.css
  script.js
  assets/
    foto-perfil.png
index.html
<!-- JavaScript: carregue no final do body para encontrar os elementos da página. -->
<script src="script.js"></script>

Se o script for colocado antes do HTML principal, ele pode tentar buscar elementos que ainda não foram criados. Por isso, neste projeto, vamos conectar o JavaScript no final da página.

Selecionando elementos da página

Antes de criar uma interação, o JavaScript precisa encontrar os elementos do HTML. Para isso, usamos document.getElementById e document.querySelectorAll.

script.js
/* Seletores principais: guardam em variáveis os elementos que o JavaScript vai controlar. */
const navbar = document.getElementById('navbar');
const hamburger = document.getElementById('hamburger');
const navLinks = document.getElementById('navLinks');
const menuLinks = document.querySelectorAll('.nav-links a');
const revealElements = document.querySelectorAll('.reveal');
const sections = document.querySelectorAll('section[id]');

Essas variáveis deixam o código mais organizado. Em vez de procurar o mesmo elemento várias vezes, você busca uma vez e reutiliza quando precisar.

Criando o menu mobile

O menu mobile precisa abrir e fechar quando a pessoa toca no botão. Também é importante atualizar atributos de acessibilidade para leitores de tela entenderem o estado do menu.

script.js
/* Menu mobile: abre, fecha e atualiza o estado visual do botão. */
function setMenuState(isOpen) {
  hamburger?.classList.toggle('open', isOpen);
  navLinks?.classList.toggle('open', isOpen);
  hamburger?.setAttribute('aria-expanded', String(isOpen));
  hamburger?.setAttribute('aria-label', isOpen ? 'Fechar menu' : 'Abrir menu');
}

/* Clique no botão: alterna entre menu aberto e menu fechado. */
hamburger?.addEventListener('click', () => {
  const isOpen = navLinks?.classList.contains('open');
  setMenuState(!isOpen);
});

O operador ?. evita erro caso o elemento não exista. Isso torna o código mais seguro enquanto você ainda está montando ou ajustando o HTML.

Fechando o menu ao clicar em um link

Quando a pessoa clica em um link do menu mobile, o esperado é que o menu feche. Isso evita que ele continue ocupando espaço depois da navegação.

script.js
/* Links do menu: depois do clique, o menu fecha automaticamente. */
menuLinks.forEach((link) => {
  link.addEventListener('click', () => {
    setMenuState(false);
  });
});

/* Tecla Escape: permite fechar o menu pelo teclado. */
document.addEventListener('keydown', (event) => {
  if (event.key === 'Escape') {
    setMenuState(false);
  }
});

Esse detalhe melhora a experiência no celular e também ajuda quem navega pelo teclado.

Mudando o menu ao rolar a página

Uma interação simples e útil é adicionar uma classe no menu quando a página passa de certo ponto da rolagem. Com CSS, essa classe pode ativar sombra ou mudar o fundo do topo.

script.js
/* Menu com rolagem: adiciona a classe scrolled depois que a página desce um pouco. */
function updateNavbarState() {
  const hasScrolled = window.scrollY > 20;
  navbar?.classList.toggle('scrolled', hasScrolled);
}

/* Estado inicial: roda uma vez quando a página carrega. */
updateNavbarState();

/* Evento de scroll: atualiza o menu enquanto a pessoa navega pela página. */
window.addEventListener('scroll', updateNavbarState, { passive: true });

O passive: true informa ao navegador que esse evento não vai bloquear a rolagem. É um detalhe pequeno, mas ajuda a manter a experiência mais fluida.

Revelando elementos ao aparecer na tela

Em vez de animar tudo de uma vez, podemos revelar cards e seções quando eles entram na tela. Para isso, usamos IntersectionObserver.

script.js
/* Reveal: observa elementos com a classe reveal e ativa visible quando entram na tela. */
const revealObserver = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;

      entry.target.classList.add('visible');
      revealObserver.unobserve(entry.target);
    });
  },
  {
    threshold: 0.12,
    rootMargin: '0px 0px -40px 0px',
  },
);

/* Registro dos elementos: cada item .reveal começa a ser observado. */
revealElements.forEach((element) => {
  revealObserver.observe(element);
});

A classe visible deve ser estilizada no CSS. O JavaScript só decide quando ela entra; o visual da animação continua responsabilidade do CSS.

Marcando o link ativo no menu

Outra melhoria é destacar no menu qual seção está visível. Isso ajuda o visitante a se localizar dentro da página.

script.js
/* Link ativo: observa as seções e marca no menu qual delas está visível. */
const sectionObserver = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;

      const id = entry.target.getAttribute('id');

      menuLinks.forEach((link) => {
        const isCurrentLink = link.getAttribute('href') === `#${id}`;
        link.classList.toggle('active', isCurrentLink);
      });
    });
  },
  {
    threshold: 0.45,
  },
);

/* Registro das seções: cada section com id passa a ser acompanhada. */
sections.forEach((section) => {
  sectionObserver.observe(section);
});

Esse bloco depende dos links do menu apontarem corretamente para os ids das seções. Exemplo: href="#projetos" combina com section id="projetos".

JavaScript completo desta etapa

Abaixo está uma versão organizada do script.js desta etapa. Ela usa comentários por bloco para explicar a intenção de cada parte sem poluir o código linha por linha.

script.js
/* 1. Seletores: elementos que o JavaScript vai controlar na página. */
const navbar = document.getElementById('navbar');
const hamburger = document.getElementById('hamburger');
const navLinks = document.getElementById('navLinks');
const menuLinks = document.querySelectorAll('.nav-links a');
const revealElements = document.querySelectorAll('.reveal');
const sections = document.querySelectorAll('section[id]');

/* 2. Menu mobile: abre, fecha e atualiza acessibilidade do botão. */
function setMenuState(isOpen) {
  hamburger?.classList.toggle('open', isOpen);
  navLinks?.classList.toggle('open', isOpen);
  hamburger?.setAttribute('aria-expanded', String(isOpen));
  hamburger?.setAttribute('aria-label', isOpen ? 'Fechar menu' : 'Abrir menu');
}

hamburger?.addEventListener('click', () => {
  const isOpen = navLinks?.classList.contains('open');
  setMenuState(!isOpen);
});

menuLinks.forEach((link) => {
  link.addEventListener('click', () => {
    setMenuState(false);
  });
});

document.addEventListener('keydown', (event) => {
  if (event.key === 'Escape') {
    setMenuState(false);
  }
});

/* 3. Menu com rolagem: adiciona sombra ou fundo diferente quando a página desce. */
function updateNavbarState() {
  const hasScrolled = window.scrollY > 20;
  navbar?.classList.toggle('scrolled', hasScrolled);
}

updateNavbarState();
window.addEventListener('scroll', updateNavbarState, { passive: true });

/* 4. Reveal: mostra elementos quando eles entram na área visível da tela. */
const revealObserver = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;

      entry.target.classList.add('visible');
      revealObserver.unobserve(entry.target);
    });
  },
  {
    threshold: 0.12,
    rootMargin: '0px 0px -40px 0px',
  },
);

revealElements.forEach((element) => {
  revealObserver.observe(element);
});

/* 5. Link ativo: destaca no menu a seção que está aparecendo na tela. */
const sectionObserver = new IntersectionObserver(
  (entries) => {
    entries.forEach((entry) => {
      if (!entry.isIntersecting) return;

      const id = entry.target.getAttribute('id');

      menuLinks.forEach((link) => {
        const isCurrentLink = link.getAttribute('href') === `#${id}`;
        link.classList.toggle('active', isCurrentLink);
      });
    });
  },
  {
    threshold: 0.45,
  },
);

sections.forEach((section) => {
  sectionObserver.observe(section);
});

Próximo passo da trilha

Agora o portfólio já tem estrutura, visual e interações básicas. A próxima parte é a responsividade: adaptar a página para celular, tablet e desktop.

A Parte 4 está disponível na URL /projetos/portfolio-css-responsivo. Nela, vamos ajustar menu, hero, grids, cards e contato para telas menores.

Fontes utilizadas