AqGoEs.
Front-end

Como Programei a Lógica JavaScript do Meu Portfólio

16/09/2026 • 6 min de leitura
Alan Queirós
Conteúdo por: Alan Queirós
AqGoEs - Dev
Compartilhe:
Resumir este post com: 💬 ChatGPT 🔍 Perplexity

📌 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-expanded para 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áriasAPI / Método UtilizadoAPI / Método Utilizado
Troca de Temadocument.startViewTransitionAnimações suaves de layout sem usar frameworks de animação pesados.
Detecção de ScrollIntersectionObserverElimina eventos repetitivos de scroll, melhorando a taxa de quadros (FPS).
Respeito às PreferênciasmatchMedia(‘(prefers-reduced-motion)’)Garante acessibilidade a usuários que sofrem com sensibilidade a animações.
Persistência de DadoslocalStorage.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 prefersReducedMotion antes 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!

💡 Veja também

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *