Como Programei a Lógica JavaScript do Meu Portfólio
📌 Navegue por tópicos
Intradução
Como Programei a Lógica JavaScript do Meu Portfólio: Menu, Dark Mode e Animações
Depois de montar toda a estrutura semântica em HTML5 e estilizar o projeto com Tailwind CSS no post anterior, chegou a hora de dar vida ao meu portfólio (aqgoes.com).
Uma interface moderna exige interatividade rápida, acessibilidade e micro-interações fluidas. Tudo isso precisa rodar sem depender de frameworks pesados como React ou Vue para tarefas simples do client-side.
Neste artigo, vou explicar o código em JavaScript Vanilla que desenvolvi para controlar quatro pilares do meu site: o menu mobile dinâmico, o alternador de Dark/Light Mode com a View Transitions API, o efeito Scroll-Spy de navegação e as animações de Scroll Reveal.
O Código JavaScript Completo do Projeto
Abaixo está o script principal que roda no projeto, estruturado dentro do evento DOMContentLoaded e considerando a API prefers-color-scheme e acessibilidade reduzida:
document.addEventListener('DOMContentLoaded', () => {
const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
// --- 1. LÓGICA DO MENU HAMBÚRGUER (abertura/fechamento suave) ---
const menuBtn = document.getElementById('menu-btn');
const menu = document.getElementById('menu');
const iconOpen = document.getElementById('icon-open');
const iconClose = document.getElementById('icon-close');
const navLinks = document.querySelectorAll('.nav-link');
function toggleMenu(forceClose = false) {
const isOpen = menu.classList.contains('menu-open');
const willOpen = forceClose ? false : !isOpen;
menu.classList.toggle('menu-open', willOpen);
iconOpen.classList.toggle('hidden', willOpen);
iconClose.classList.toggle('hidden', !willOpen);
menuBtn.setAttribute('aria-expanded', String(willOpen));
}
menuBtn.addEventListener('click', () => toggleMenu());
navLinks.forEach(link => {
link.addEventListener('click', () => {
if (window.innerWidth < 768) {
toggleMenu(true);
}
});
});
// Fecha o menu mobile se a tela for redimensionada para desktop
window.addEventListener('resize', () => {
if (window.innerWidth >= 768) {
toggleMenu(true);
}
});
// --- 2. LÓGICA DO DARK / LIGHT MODE (com View Transitions API) ---
const themeToggles = document.querySelectorAll('.theme-toggle');
const htmlElement = document.documentElement;
function updateThemeUI(isLight) {
themeToggles.forEach(btn => {
const moon = btn.querySelector('.icon-moon');
const sun = btn.querySelector('.icon-sun');
moon.classList.toggle('hidden', isLight);
sun.classList.toggle('hidden', !isLight);
});
}
function applyTheme() {
const isLight = htmlElement.classList.toggle('light');
htmlElement.classList.toggle('dark', !isLight);
localStorage.setItem('theme', isLight ? 'light' : 'dark');
updateThemeUI(isLight);
}
function toggleTheme() {
// Transição suave com fallback para navegadores sem suporte
if (!prefersReducedMotion && document.startViewTransition) {
document.startViewTransition(() => applyTheme());
} else {
applyTheme();
}
}
themeToggles.forEach(btn => btn.addEventListener('click', toggleTheme));
// Carrega o tema salvo do usuário
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'light') {
htmlElement.classList.add('light');
htmlElement.classList.remove('dark');
updateThemeUI(true);
} else {
htmlElement.classList.add('dark');
updateThemeUI(false);
}
// --- 3. SCROLL-SPY — destaca o link da seção visível ---
const sections = document.querySelectorAll('section[id]');
const sectionLinks = document.querySelectorAll('.nav-link[data-section]');
if ('IntersectionObserver' in window && sections.length) {
const spyObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const id = entry.target.getAttribute('id');
sectionLinks.forEach(link => {
link.classList.toggle('active', link.dataset.section === id);
});
}
});
}, { rootMargin: '-45% 0px -50% 0px', threshold: 0 });
sections.forEach(section => spyObserver.observe(section));
}
// --- 4. ANIMAÇÃO DE ENTRADA — elementos aparecem ao rolar ---
const revealEls = document.querySelectorAll('.reveal');
if ('IntersectionObserver' in window && revealEls.length && !prefersReducedMotion) {
const revealObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('in-view');
revealObserver.unobserve(entry.target);
}
});
}, { threshold: 0.15 });
revealEls.forEach(el => revealObserver.observe(el));
} else {
revealEls.forEach(el => el.classList.add('in-view'));
}
});
Desmontando a Lógica: Como Cada Parte Funciona
Para facilitar o entendimento, dividi o script nas quatro principais funcionalidades da aplicação:
1. Menu Mobile Limpo e Acessível
A função toggleMenu() manipula a classe .menu-open no nav e alterna os ícones do menu hambúrguer. Além do clique no botão, adicionei dois detalhes importantes de experiência do usuário:
- Fechamento Automático: Ao clicar em qualquer link interno no mobile, o menu fecha sozinho para revelar o conteúdo.
- Ajuste de Resize: Se a janela for redimensionada para uma largura de computador (
≥768px), o estado mobile é resetado automaticamente. - Acessibilidade: Atualização do atributo
aria-expandedpara leitores de tela.
2. Dark/Light Mode com View Transitions API
Além de salvar a preferência do usuário no localStorage, usei a moderna View Transitions API (document.startViewTransition).
Essa tecnologia cria uma animação visual de transição fluida entre as trocas de cor de fundo. Quando o navegador não suporta a API ou o usuário solicitou menos animações no sistema operacional, o script faz o fallback direto para a função applyTheme().
3. Scroll-Spy com IntersectionObserver
O recurso de Scroll-Spy ativa a classe .active no menu superior indicando exatamente em qual seção a página está posicionada.
Em vez de registrar eventos de window.onscroll (que consomem muito processamento), usei a API nativa IntersectionObserver. Com o parâmetro rootMargin: '-45% 0px -50% 0px', a troca de estado ocorre exatamente quando a seção cruza o meio da tela.
4. Animação Scroll Reveal Sem Bibliotecas Pesadas
As seções marcadas com a classe .reveal ganham a classe .in-view à medida que entram na área visível da tela. Quando o elemento é animado uma vez, o observador deixa de monitorá-lo com revealObserver.unobserve(), economizando memória do navegador.
Tabela comparativa de APIs Web Utilitárias
| Tabela comparativa de APIs Web Utilitárias | API / Método Utilizado | API / Método Utilizado |
| Troca de Tema | document.startViewTransition | Animações suaves de layout sem usar frameworks de animação pesados. |
| Detecção de Scroll | IntersectionObserver | Elimina eventos repetitivos de scroll, melhorando a taxa de quadros (FPS). |
| Respeito às Preferências | matchMedia(‘(prefers-reduced-motion)’) | Garante acessibilidade a usuários que sofrem com sensibilidade a animações. |
| Persistência de Dados | localStorage.getItem() | Mantém o tema escolhido ativo mesmo após o recarregamento do site. |
Destaques de Acessibilidade e Performance
💡 Dica de Ouro: Sempre verifique a constante
prefersReducedMotionantes de disparar animações automáticas em um site. Respeitar as configurações do sistema operacional do usuário é uma das regras mais importantes para acessibilidade web avançada (WCAG)
Conclusão
Com menos de 100 linhas de JavaScript puro, consegui entregar uma interface rápida, interativa e rica em micro-animações no aqgoes.com. A escolha por APIs nativas como IntersectionObserver e startViewTransition garante que o portfólio carregue instantaneamente e com nota máxima nas ferramentas de audit como o Google Lighthouse.
Você já conhecia a View Transitions API para alternar temas ou ainda usa classes CSS tradicionais? Comente abaixo como costuma implementar essa funcionalidade nos seus projetos!