Capítulo 21
Galeria e bibliotecas
4 min de leitura 700 palavras 1 gravura
A Oficina do Farol tem doze imagens ilustrativas de estudo: cerâmica, xilo, farol, mar. Mostrar as doze ao mesmo tempo cansa. A galeria mostra uma. CSS esconde o resto. JavaScript troca a classe .ativa. Depois você reusa a mesma ideia com desliza.js, um arquivo local com licença MIT na pasta. Sem CDN. Sem biblioteca na rede.
O exemplo está em exemplos/21-galeria/. Abra index.html para a versão feita à mão. Abra com-biblioteca.html para a versão com desliza.js.
Trabalhe na cópia inicio desta etapa. Leia e digite o código indicado; mude apenas uma coisa. Antes de recarregar, escreva o resultado que espera. Depois compare com a referência completa ao lado. Se algo falhar, confira primeiro o arquivo salvo e o caminho aberto.
O que você vai aprender
- Esconder fotos com
display: nonee mostrar com.ativa. - Desativar Anterior na primeira e Próxima na última.
- Reaproveitar a lógica numa biblioteca local, sem baixar nada.
- Animar a foto visível com
@keyframes, só CSS.
CSS esconde, a classe mostra
.foto {
display: none;
max-width: 100%;
height: auto;
}
.foto.ativa {
display: block;
}
display: none tira o elemento do fluxo: ele não ocupa espaço. Quem tem .ativa volta a aparecer. Só uma foto deve ter essa classe no HTML inicial.
<img src="imagens/01.png" alt="Foto ilustrativa 1 da Oficina do Farol" class="foto ativa" width="900" height="560">
<img src="imagens/02.png" alt="Foto ilustrativa 2 da Oficina do Farol" class="foto" width="900" height="560">
Os botões:
<button type="button" class="anterior" disabled>Anterior</button>
<button type="button" class="proxima">Próxima</button>
Na primeira foto, Anterior já nasce disabled. Não há para onde voltar.
Erro comum: usar visibility: hidden achando que é o mesmo. O buraco da imagem continua lá. Nesta galeria, display: none é o combinado.

Anterior e próxima
let atual = 0;
const fotos = document.querySelectorAll(".foto");
const anterior = document.querySelector(".anterior");
const proxima = document.querySelector(".proxima");
function ir(proximo) {
fotos[atual].classList.remove("ativa");
atual = proximo;
fotos[atual].classList.add("ativa");
anterior.disabled = atual === 0;
proxima.disabled = atual === fotos.length - 1;
}
anterior.addEventListener("click", () => ir(atual - 1));
proxima.addEventListener("click", () => ir(atual + 1));
atual é o índice, começando em 0. Você tira .ativa da foto velha, põe na nova, e atualiza disabled. Na última, Próxima apaga. Na primeira, Anterior apaga.
Não precisa “proteger” o clique além disso: botão desabilitado não dispara o fluxo útil. Mesmo assim, não chame ir(-1) à mão.
Pare e experimente. Clique Próxima até o fim. O botão deve esfriar. Recarregue. Anterior deve nascer inútil, Próxima útil.
A mesma ideia com desliza.js local
Uma biblioteca é código de outra pessoa (ou do volume) que você chama, em vez de reescrever o carrossel. Neste material a biblioteca está na pasta, arquivo desliza.js, licença MIT em LICENCA-desliza.txt. Não cole um endereço de CDN. Sem rede, a galeria continua abrindo.
<div id="carrossel">
<figure data-slide>
<img src="imagens/01.png" alt="Foto ilustrativa 1">
</figure>
<figure data-slide hidden>
<img src="imagens/02.png" alt="Foto ilustrativa 2">
</figure>
<p>
<button type="button" data-prev>Anterior</button>
<button type="button" data-next>Próxima</button>
</p>
</div>
<script src="desliza.js"></script>
<script>Desliza("#carrossel");</script>
data-slide marca cada foto. hidden é o HTML que esconde. Desliza("#carrossel") liga os botões data-prev e data-next.
Trecho da biblioteca:
function Desliza(seletor) {
const raiz = document.querySelector(seletor);
const slides = [...raiz.querySelectorAll("[data-slide]")];
let atual = 0;
function mostrar() {
slides.forEach((slide, i) => {
slide.hidden = i !== atual;
});
}
raiz.addEventListener("click", (evento) => {
if (evento.target.matches("[data-prev]") && atual > 0) atual -= 1;
if (evento.target.matches("[data-next]") && atual < slides.length - 1) atual += 1;
mostrar();
});
mostrar();
}
É a mesma conta de índice. Só muda quem esconde: atributo hidden em vez de .ativa.
Erro comum: apontar o script para um site externo “porque é mais moderno”. Neste volume a biblioteca viaja com o zip. Se o arquivo sumir, a página não desliza — e você percebe no disco, não na rede.
Animação CSS, sem pedir nada à internet
@keyframes descreve o caminho da animação. O navegador interpola. Não precisa de script de movimento nem de pacote baixado.
@keyframes pulsa {
from { transform: scale(1); }
to { transform: scale(1.03); }
}
.foto.ativa {
display: block;
animation: pulsa 0.35s ease;
}
Quando a classe .ativa entra, a foto dá um pulso curto. ease começa mais rápido e termina mais manso. Sem infinite, o pulso roda uma vez.
Você pode animar opacidade (from { opacity: 0 }) no lugar da escala. Fique no CSS. Movimento complexo com biblioteca remota fica fora desta etapa.
Atividades
- O que acontece se duas fotos tiverem
.ativaao mesmo tempo? - Por que Anterior nasce com
disabled? - Onde está a licença de
desliza.jse por que ela acompanha o arquivo? - No exemplo feito à mão, troque o
altda terceira foto. Em qual clique a mudança aparece na tela?
Respostas comentadas
- As duas ficam visíveis, uma abaixo da outra. O combinado da galeria quebra. O script assume uma só
.ativa. - O índice inicial é 0. Não existe foto antes da primeira. Deixar o botão ativo convidaria um
ir(-1)sem foto. - Em
LICENCA-desliza.txt, na mesma pasta. MIT pede que o aviso viaje com o código. Redistribuir sem o texto da licença descumpre o combinado. - No segundo clique em Próxima (fotos 1 → 2 → 3). O
altnão muda o desenho; ele descreve a imagem para quem não a vê.
Referências
O arquivo completo
Referência do exemplo 21-galeria, reproduzida aqui para que você possa conferir sem sair da página.
galeria.js
let atual = 0;
const fotos = document.querySelectorAll(".foto");
const anterior = document.querySelector(".anterior");
const proxima = document.querySelector(".proxima");
function ir(proximo) {
fotos[atual].classList.remove("ativa");
atual = proximo;
fotos[atual].classList.add("ativa");
anterior.disabled = atual === 0;
proxima.disabled = atual === fotos.length - 1;
}
anterior.addEventListener("click", () => ir(atual - 1));
proxima.addEventListener("click", () => ir(atual + 1));
galeria.css
body { font-family: "Segoe UI", Tahoma, sans-serif; background: #fffaf3; margin: 0; padding: 1rem; }
.galeria { display: flex; justify-content: center; min-height: 20rem; }
.foto { display: none; max-width: 100%; height: auto; }
.foto.ativa { display: block; }
button { font: inherit; padding: 0.5rem 1rem; margin-right: 0.5rem; }
button:focus { outline: 3px solid #c45c26; }
button:disabled { opacity: 0.5; }