1. Por que esta aula existe
Nas aulas 7 e 8 você aprendeu o suficiente para que o front funcione: EJS renderiza templates no servidor, o DOM responde a eventos no cliente, e o fetch conversa com a API. Funciona — mas frequentemente parece feito por uma equipe diferente em cada página: cada arquivo CSS reinventa cores, tipografia e espaçamento; cada arquivo JavaScript repete lógica de manipulação de DOM; um onclick="..." espalhado no HTML aqui, uma função global no window ali. É o ponto em que muitas equipes se rendem e instalam um framework — Vue, React, Tailwind — para esconder a bagunça atrás de mais ferramental.
Esta aula propõe o caminho contrário: dominar o que CSS e JavaScript modernos já fazem nativamente, sem framework algum. Você vai aprender CSS arquitetural com variáveis, BEM e organização por responsabilidade; layouts profissionais com Grid e Flexbox; animações com performance de GPU; ES Modules nativos no browser; classes e closures como ferramentas de encapsulamento; e localStorage/FormData como APIs prontas para persistência e envio de formulários. Ao final, o front do projeto deve estar arrumado, testável e — talvez o mais importante — agradável de evoluir.
O backend permanece TypeScript + Express + pg servindo HTML via EJS (SSR). Os arquivos JavaScript da aula de hoje rodam no cliente (browser). Não há ferramentas de build extras: o navegador moderno já entende ESM, classes e tudo o que veremos aqui.
2. CSS Variables — custom properties
CSS Variables (oficialmente "custom properties") são variáveis nativas do CSS, declaradas com prefixo -- e consumidas via var(--nome). Diferentemente das variáveis de pré-processadores como SASS, elas existem em runtime: são parte do CSSOM, herdam pela árvore do DOM, podem ser sobrescritas em qualquer escopo (um seletor mais especÃfico, uma media query, um data-*) e podem ser lidas e escritas via JavaScript. Isso muda a forma como você organiza o design system.
O lugar canônico para declará-las é :root, que representa o elemento <html>. Variáveis declaradas ali ficam disponÃveis para toda a página. Em projetos pequenos, cinco ou seis variáveis (cores principais, raio de borda, espaçamento base, cor de texto) já cobrem a maior parte do design. À medida que cresce, agrupe-as por categoria: --color-*, --space-*, --font-*, --radius-*.
:root {
--color-primary: #4b5563;
--color-surface: #ffffff;
--color-text: #0f172a;
--color-muted: #64748b;
--space-1: .5rem;
--space-2: 1rem;
--space-3: 1.5rem;
--radius: 12px;
}
.btn {
background: var(--color-primary);
color: var(--color-surface);
padding: var(--space-1) var(--space-2);
border-radius: var(--radius);
}
Tematização viva: dark mode com data-theme
O caso clássico de CSS Variables é dark mode. Declare as variáveis "claras" no :root e sobrescreva-as em um seletor de tema. Para alternar, basta mudar um atributo no <html>:
:root {
--surface: #ffffff;
--text: #0f172a;
}
[data-theme="dark"] {
--surface: #0f172a;
--text: #e2e8f0;
}
.card {
background: var(--surface);
color: var(--text);
}
const btn = document.querySelector('#toggle-theme');
btn.addEventListener('click', () => {
const atual = document.documentElement.dataset.theme;
const novo = atual === 'dark' ? 'light' : 'dark';
document.documentElement.dataset.theme = novo;
localStorage.setItem('tema', novo);
});
A media query @media (prefers-color-scheme: dark) diz se o usuário usa tema escuro no sistema operacional. Combine-a com o data-theme escolhido manualmente: prefira a escolha do usuário, mas use a do sistema como default.
3. Flexbox — layout em uma dimensão
Flexbox é o módulo de layout do CSS para distribuir elementos ao longo de um eixo — seja horizontal (linha) ou vertical (coluna). Ativa-se aplicando display: flex em um container; todos os filhos diretos viram "itens flexÃveis". A partir daÃ, você controla três coisas: direção (flex-direction: row, column), distribuição no eixo principal (justify-content) e alinhamento no eixo perpendicular (align-items). O gap define o espaço entre itens — bem mais limpo que margin.
O caso de uso mais comum é a navbar: logo à esquerda, links centralizados, ações à direita. justify-content: space-between separa os blocos, align-items: center alinha verticalmente. Outro caso clássico é a linha de cards de tamanhos iguais: um único flex: 1 em cada card distribui o espaço uniformemente. Para listas que precisam quebrar linha em telas estreitas, flex-wrap: wrap.
/* Navbar: logo | links | ações */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
padding: 1rem 1.5rem;
}
/* Linha de cards de tamanhos iguais */
.card-row { display: flex; gap: 12px; flex-wrap: wrap; }
.card-row .card { flex: 1 1 240px; } /* grow shrink basis */
/* Centralizar conteúdo em qualquer altura */
.center-box {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
}
A propriedade composta flex: 1 1 240px é equivalente a flex-grow: 1; flex-shrink: 1; flex-basis: 240px — o item começa em 240px, cresce se sobrar espaço e encolhe se faltar. É a forma idiomática de dizer "esse item é elástico, com tamanho mÃnimo razoável".
justify-content sempre opera no eixo principal (definido por flex-direction). align-items opera no eixo cruzado. Quando flex-direction: column, isso se inverte: justify-content passa a controlar verticalmente, e align-items, horizontalmente. Confundir os dois é o erro mais comum em flex.
4. Grid — layout em duas dimensões
Grid é o módulo de layout para casos onde você precisa controlar simultaneamente linhas e colunas. Onde Flex é "uma fila", Grid é "uma planilha". O container é declarado com display: grid, e você define a estrutura via grid-template-columns, grid-template-rows e, opcionalmente, grid-template-areas para nomear regiões.
O caso mais imediato é o "grid de 12 colunas" — herança do Bootstrap, hoje feito em CSS puro: grid-template-columns: repeat(12, 1fr). Cada item pode então ocupar um intervalo de colunas com grid-column: span 4. Mas o padrão moderno mais útil é o grid auto-fit + minmax, que produz uma listagem responsiva sem media queries:
/* Tantas colunas de 240px+ quanto couberem; o resto vira 1fr */
.cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
gap: 16px;
}
/* Layout com áreas nomeadas */
.app {
display: grid;
grid-template-columns: 240px 1fr;
grid-template-rows: auto 1fr auto;
grid-template-areas:
"header header"
"side main "
"footer footer";
min-height: 100vh;
}
.app__header { grid-area: header; }
.app__side { grid-area: side; }
.app__main { grid-area: main; }
.app__footer { grid-area: footer; }
A unidade fr (fraction) representa "uma fração do espaço restante depois dos tamanhos fixos". 1fr 2fr em duas colunas dá ⅓ e ⅔. repeat(N, 1fr) cria N colunas iguais. minmax(min, max) diz "esta coluna tem entre min e max" — combinada com auto-fit, o navegador acomoda quantas couberem.
Flex: layout linear (uma direção), conteúdo dita o tamanho. Grid: layout bidimensional, layout dita o tamanho. É comum combinar os dois: Grid para a página, Flex dentro de cada componente. Não há rivalidade — eles resolvem problemas diferentes.
5. Mobile-first, clamp() e unidades fluidas
"Mobile-first" não é uma frase de marketing — é uma estratégia técnica. Em vez de escrever os estilos para desktop e depois "consertar" no celular, você começa pelos estilos do celular (que são naturalmente mais simples) e adiciona melhorias para telas maiores via @media (min-width: ...). O CSS resultante é menor, a experiência no celular é mais rápida, e você raramente precisa "desfazer" estilos.
/* Base: mobile (sem media query) */
.container { padding: 1rem; }
.cards { grid-template-columns: 1fr; }
/* Tablet pra cima */
@media (min-width: 768px) {
.container { padding: 1.5rem; }
.cards { grid-template-columns: repeat(2, 1fr); }
}
/* Desktop pra cima */
@media (min-width: 1024px) {
.container { max-width: 1100px; margin: 0 auto; }
.cards { grid-template-columns: repeat(3, 1fr); }
}
clamp() e tipografia fluida
A função clamp(min, ideal, max) retorna o valor ideal, mas limitado entre o mÃnimo e o máximo. Combinada com unidades viewport (vw), produz tipografia que cresce suavemente com a largura da tela, sem media queries:
h1 { font-size: clamp(1.5rem, 4vw, 2.5rem); }
h2 { font-size: clamp(1.25rem, 3vw, 2rem); }
.hero{ padding: clamp(2rem, 8vw, 6rem) 1rem; }
| Unidade | Quando usar | Por quê |
|---|---|---|
rem | Tipografia, espaçamento global | Escala com a preferência de zoom do usuário (acessibilidade) |
em | Padding interno de componentes | Escala com a fonte local — bom para botões |
% | Larguras dentro de containers | Relativo ao pai |
vw / vh | Hero, telas inteiras, com clamp | Relativo à viewport |
px | Bordas, sombras, Ãcones pequenos | Quando o valor exato importa visualmente |
6. Posicionamento — relative, absolute, fixed, sticky
A propriedade position tira (parcialmente ou totalmente) o elemento do fluxo normal do documento. relative mantém o elemento no fluxo, mas permite ajustá-lo com top/left/right/bottom; serve principalmente como referência para filhos absolutos. absolute remove o elemento do fluxo e o posiciona em relação ao ancestral posicionado mais próximo (que tenha position diferente de static). fixed posiciona em relação à viewport — útil para barras de navegação fixas e modais. sticky é hÃbrido: comporta-se como relative até atingir um limite de scroll, então "gruda" como fixed.
/* Badge no canto superior direito de um card */
.card { position: relative; }
.card__badge {
position: absolute;
top: 8px; right: 8px;
}
/* Header sticky no topo da página */
.site-header {
position: sticky;
top: 0;
z-index: 100;
background: var(--surface);
}
O stacking context determina a ordem em que elementos sobrepõem-se. Cada elemento posicionado com z-index diferente de auto cria um novo contexto. Dentro de um contexto, os z-index são comparados entre si — não com elementos de outros contextos. Por isso, às vezes um z-index: 9999 "não funciona": o elemento está dentro de um contexto que, como um todo, fica abaixo de outro.
7. Transitions e animações com performance
transition é o mecanismo mais simples de animação: você define quais propriedades animar, por quanto tempo e com qual função de easing. Quando uma dessas propriedades muda (por hover, por mudança de classe, por JavaScript), o navegador interpola entre o valor antigo e o novo.
.btn {
transition: transform .2s ease, box-shadow .2s ease;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 6px 18px rgba(0,0,0,.18);
}
@keyframes fadeUp {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.toast { animation: fadeUp .35s ease both; }
A regra de ouro: transform e opacity
O navegador renderiza em três etapas: layout (posições e tamanhos), paint (pixels de cores e bordas) e composite (sobreposição de camadas na GPU). Animar propriedades de layout — width, height, top, left, margin, padding — força o navegador a recalcular layout em cada frame, o que custa muito caro.
transform e opacity, ao contrário, são tratadas pelo compositor: a GPU interpola sem reflow. Sempre que possÃvel, troque animações de width por scaleX, e animações de top por translateY. Em listas grandes ou em dispositivos modestos, isso é a diferença entre 60 fps e travamento perceptÃvel.
Algumas pessoas têm vestibular sensÃvel a animações grandes. Respeite o sistema delas:
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .01ms !important;
transition-duration: .01ms !important;
}
}
8. Pseudo-classes e pseudo-elementos
Pseudo-classes (:hover, :focus, :checked, :disabled, :not()) selecionam elementos com base em estado. Pseudo-elementos (::before, ::after, ::first-letter, ::selection) selecionam partes de um elemento ou criam conteúdo que não existe no HTML. Use um único dois-pontos para pseudo-classes, dois para pseudo-elementos — convenção do CSS3.
Particularmente importantes para acessibilidade são :focus-visible (foco vindo de teclado, não de clique) e :focus-within (qualquer descendente está focado). Use :focus-visible para destacar campos quando o usuário navega via Tab, sem o "anel" antiestético no clique do mouse.
.input:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
.list li:not(:last-child) { border-bottom: 1px solid #eee; }
/* Tooltip via ::after */
.tooltip { position: relative; }
.tooltip::after {
content: attr(data-tip);
position: absolute; bottom: 100%; left: 50%;
transform: translateX(-50%);
background: #0f172a; color: #fff;
padding: 4px 8px; border-radius: 6px;
opacity: 0; transition: opacity .2s;
}
.tooltip:hover::after { opacity: 1; }
9. Especificidade e cascata
Quando duas regras se aplicam ao mesmo elemento, qual ganha? A resposta vem de quatro critérios, em ordem: origem (estilos do navegador, do usuário, do autor), importância (!important sobe um patamar), especificidade e, em empate, ordem (a última declarada vence).
A especificidade é calculada como uma tupla (a, b, c): a = número de IDs, b = número de classes/pseudo-classes/atributos, c = número de elementos/pseudo-elementos. Compara-se da esquerda para a direita. #header .menu li tem (1, 1, 1); .nav .active tem (0, 2, 0); o primeiro vence.
!important é o "GOTO" do CSS: resolve o problema imediato e cria três outros maiores. Use só em três casos legÃtimos: (1) estilos de utilitário em uma biblioteca onde o usuário pode sobrescrever via classe; (2) overrides em prefers-reduced-motion; (3) hacks de terceiros que você não controla. Em código novo, é sinal de que sua especificidade está mal modelada.
Estratégias para evitar cascata complicada: usar BEM (todas as classes têm a mesma especificidade), evitar #id como seletor de estilo, e manter no máximo dois ou três nÃveis de aninhamento.
10. BEM vs utility-first
Sem framework, você precisa de uma convenção de nomes para que o CSS escale sem virar bagunça. As duas abordagens dominantes hoje são BEM e utility-first (estilo Tailwind).
BEM — Block, Element, Modifier
BEM define classes com sintaxe estrita: .bloco, .bloco__elemento, .bloco--modificador. Cada classe descreve o que o elemento é, não como ele se parece. O CSS resultante é raso (todas as classes têm especificidade equivalente — (0, 1, 0)) e os componentes são autocontidos.
<!-- HTML -->
<article class="card card--featured">
<h3 class="card__title">Produto X</h3>
<p class="card__desc">Descrição curta</p>
<span class="card__badge card__badge--sale">-20%</span>
</article>
/* CSS */
.card { background: var(--surface); border-radius: 12px; padding: 1rem; }
.card__title { font-size: 1.1rem; font-weight: 700; }
.card__desc { color: var(--muted); margin-top: .5rem; }
.card__badge { padding: 2px 8px; border-radius: 999px; }
.card__badge--sale { background: #ef4444; color: white; }
.card--featured { border-left: 4px solid var(--color-primary); }
| Aspecto | BEM | Utility-first (Tailwind) |
|---|---|---|
| Classes no HTML | 1–3 semânticas | 10–30 utilitárias |
| Onde mora o estilo | Em .css separado | Inline, no HTML |
| Curva de leitura | HTML limpo, CSS denso | HTML denso, CSS quase nulo |
| Reuso de componente | Natural via classe | Repetir utilities ou criar @apply |
| Refactor de design | Mexe-se em poucos lugares | Find & replace em todo HTML |
| Adequado a este projeto | Sim — recomendado | Exige build e tooling extra |
Para o projeto desta disciplina, BEM é a escolha recomendada: combina bem com EJS (que já tem componentes via partials) e dispensa qualquer build de CSS. Os componentes do projeto serão .card, .button, .form, .list, etc., cada um com seu arquivo CSS dedicado.
11. Organização do CSS no projeto SSR + EJS
Como o projeto é servido por Express + EJS, o CSS vive em public/css/, servido via express.static. A organização sugerida divide os arquivos por responsabilidade:
public/
└── css/
├── base.css # reset, :root variables, tipografia base
├── layout.css # containers, grids de página, header/footer
├── components.css # .card, .btn, .form, .badge, ...
├── utilities.css # helpers (.text-center, .mt-2, .visually-hidden)
└── theme.css # overrides de [data-theme="dark"]
No template EJS principal (views/layout.ejs ou views/partials/head.ejs), inclua os arquivos na ordem certa: base → layout → components → utilities → theme. Em produção, considere concatenar e minificar em public/css/app.min.css; em dev, deixe separados para facilitar inspeção no DevTools.
<link rel="stylesheet" href="/css/base.css">
<link rel="stylesheet" href="/css/layout.css">
<link rel="stylesheet" href="/css/components.css">
<link rel="stylesheet" href="/css/utilities.css">
<link rel="stylesheet" href="/css/theme.css">
12. Acessibilidade visual — o mÃnimo absoluto
Acessibilidade visual não é "extra de design": é a diferença entre uma interface utilizável por todos e uma que exclui parte dos usuários. Três princÃpios que cabem em qualquer aula introdutória de CSS:
- Contraste de cor: texto normal precisa ter razão de contraste mÃnima de 4.5:1 contra o fundo (WCAG AA). Texto grande, 3:1. Use ferramentas como o WebAIM Contrast Checker para validar — não confie nos olhos.
- Foco visÃvel: nunca remova o outline sem dar uma alternativa. O usuário que navega por Tab precisa ver onde está.
:focus-visible+ um anel de foco de alta contraste é o padrão correto. - Movimento opt-out: respeite
prefers-reduced-motion. Animações grandes podem provocar náusea em pessoas com transtornos vestibulares.
Em mobile, áreas clicáveis devem ter pelo menos 44×44px (recomendação iOS) ou 48×48dp (Material). Nunca tenha botões ou links menores que isso, especialmente em formulários.
13. ES Modules nativos no browser
Até alguns anos atrás, JavaScript no browser era um único arquivo gigante anexado via <script>. Para "modularizar", as equipes recorriam a IIFEs, namespaces no window, AMD, RequireJS, ou bundlers como Webpack. Hoje, todos os navegadores modernos suportam ES Modules nativos — você pode usar import e export diretamente, sem nenhum build. Basta adicionar type="module" ao tag script.
<!-- views/products/index.ejs -->
<script type="module" src="/js/products/list.js"></script>
// public/js/products/list.js
import { debounce } from '../utils/debounce.js';
import ProductCard from './ProductCard.js';
import * as filtros from './filtros.js';
// public/js/utils/debounce.js
export function debounce(fn, ms = 200) {
let id;
return (...args) => {
clearTimeout(id);
id = setTimeout(() => fn(...args), ms);
};
}
// public/js/products/ProductCard.js
export default class ProductCard { /* ... */ }
CaracterÃsticas importantes
- Caminhos relativos com extensão: no browser,
import './x.js'funciona;import './x'não. - Defer automático: scripts
type="module"aguardam o HTML parsear antes de executar. - Strict mode ligado por padrão.
- Escopo de módulo: nada vaza para
window; importações e exports são explÃcitos. - Import dinâmico:
const m = await import('./pesado.js')carrega sob demanda — útil para code splitting manual.
Cada módulo tem acesso ao próprio caminho via import.meta.url, útil para resolver assets relativos ao módulo. Em ambiente Node, import.meta também existe e é frequentemente usado para encontrar o diretório do arquivo.
14. Destructuring, spread e rest
Destructuring é a sintaxe que extrai valores de objetos e arrays para variáveis em uma única linha. Spread (...x em uma chamada ou literal) "espalha" elementos. Rest (...x em uma assinatura ou no destructuring) coleta o que sobra. As três usam o mesmo operador ..., mas em posições diferentes.
// Destructuring de objeto (com renomeação e default)
const user = { id: 1, nome: 'Ana', email: 'ana@inteli.edu' };
const { nome, email: contato, telefone = 'sem telefone' } = user;
// Destructuring de array
const [primeiro, segundo, ...resto] = [10, 20, 30, 40];
// Spread — clonar objetos sem mutar
const userAtualizado = { ...user, nome: 'Ana Lima' };
// Spread em arrays
const listaNova = [...listaAntiga, novoItem];
// Rest em parâmetros de função
function somar(...numeros) {
return numeros.reduce((s, n) => s + n, 0);
}
somar(1, 2, 3, 4); // 10
// Destructuring direto em parâmetros
function criar({ nome, email, role = 'user' }) {
return { nome, email, role };
}
O padrão de "atualização imutável" ({ ...obj, campo: novoValor }) é a forma idiomática de modificar objetos sem mutar — boa prática que facilita debug e previne bugs sutis em estado compartilhado.
15. Optional chaining e nullish coalescing
Dois operadores recentes que substituem dezenas de linhas de "if (x && x.y && x.y.z)". Optional chaining (?.) acessa propriedades aninhadas e retorna undefined em vez de lançar TypeError quando algum nÃvel é null ou undefined. Nullish coalescing (??) é como ||, mas só usa o default quando o valor é null ou undefined — preserva 0, '' e false.
// ?. evita TypeError em encadeamentos
const cidade = user?.endereco?.cidade; // undefined se user/endereco for nullish
const primeiro = pedidos?.[0]?.total; // safe array indexing
const resultado = obj.metodo?.(); // só chama se método existe
// ?? respeita 0 e ''
const qtd = pedido.qtd ?? 1; // se 0 vier, mantém 0
const apelido = user.apelido || 'anônimo';// CUIDADO: '' vira 'anônimo'
const apelidoOk= user.apelido ?? 'anônimo';// só substitui se for null/undefined
|| usa o segundo valor sempre que o primeiro é falsy (0, '', false, null, undefined, NaN). ?? só usa quando é nullish (null ou undefined). Para defaults, ?? é quase sempre o correto.
16. Classes e herança
A sintaxe class no JavaScript é "açúcar sintático" sobre o sistema de herança via protótipo, mas dispensa entender prototype para começar. Você define propriedades, métodos, métodos estáticos e — desde 2022 — campos privados com #.
export default class ProductCard {
// campo privado (não acessÃvel de fora da classe)
#el;
constructor(produto) {
this.produto = produto;
this.#el = this.render();
}
render() {
const el = document.createElement('article');
el.className = 'card';
el.innerHTML = `
<h3 class="card__title">${this.produto.nome}</h3>
<p class="card__price">R$ ${this.produto.preco.toFixed(2)}</p>
`;
return el;
}
mount(parent) {
parent.appendChild(this.#el);
}
destroy() {
this.#el.remove();
}
// método estático: chamado direto na classe, não na instância
static fromJSON(json) {
return new ProductCard(JSON.parse(json));
}
}
// herança
class FeaturedProductCard extends ProductCard {
render() {
const el = super.render();
el.classList.add('card--featured');
return el;
}
}
O this dentro de métodos refere-se à instância. Cuidado ao passar métodos como callbacks: o this pode se perder. Use arrow functions ou .bind(this) para preservá-lo. Métodos definidos como arrow functions em campos de classe (onClick = () => {...}) já vêm "amarrados" à instância.
17. Closures — funções com memória
Uma closure é uma função que "lembra" das variáveis do escopo onde foi criada, mesmo depois desse escopo ter terminado. Em outras palavras, o JavaScript não destrói variáveis se ainda houver alguma função capaz de acessá-las. Closures são a base de muitos padrões úteis: contadores privados, memoização, debounce/throttle, factories de funções.
// Contador privado: valor preso na closure, inacessÃvel de fora
export function criarContador(inicio = 0) {
let valor = inicio; // preso na closure
return {
inc: () => ++valor,
dec: () => --valor,
get: () => valor,
reset: () => { valor = inicio; },
};
}
const c = criarContador();
c.inc(); c.inc(); c.get(); // 2 — não há como acessar `valor` diretamente
// Memoização: cache de resultados
function memoize(fn) {
const cache = new Map(); // preso na closure
return (key) => {
if (!cache.has(key)) cache.set(key, fn(key));
return cache.get(key);
};
}
Closures são também o motivo pelo qual let em loops for "captura" o Ãndice corretamente, enquanto var captura uma única variável compartilhada. Em código moderno, sempre use let/const — os bugs com var em closures eram um clássico do JS antigo.
18. Higher-order functions — map, filter, reduce
Higher-order functions são funções que recebem outras funções como parâmetro ou retornam funções. Array.prototype fornece um conjunto poderoso delas que substitui a maioria dos loops for:
| Método | Retorna | Uso |
|---|---|---|
map(fn) | Novo array | Transformar cada elemento |
filter(fn) | Novo array | Selecionar elementos |
reduce(fn, init) | Valor único | Acumular/agregar |
find(fn) | Primeiro match | Buscar item especÃfico |
some(fn) | true/false | Existe pelo menos um? |
every(fn) | true/false | Todos atendem? |
forEach(fn) | undefined | Side effect (raramente ideal) |
const produtos = [
{ id: 1, nome: 'Camisa', preco: 89.9, ativo: true },
{ id: 2, nome: 'Tênis', preco: 249.0, ativo: true },
{ id: 3, nome: 'Boné', preco: 49.9, ativo: false },
];
// Pipeline encadeado
const totalAtivos = produtos
.filter(p => p.ativo) // só ativos
.map(p => p.preco) // vira lista de preços
.reduce((soma, p) => soma + p, 0); // soma tudo
// Existência
const temCaro = produtos.some(p => p.preco > 200);
const todosBaratos = produtos.every(p => p.preco < 500);
19. localStorage, sessionStorage e cookies
O navegador oferece três formas principais de persistir dados no cliente. Escolher errado leva a bugs sutis ou a problemas de segurança.
| Recurso | Tamanho | Tempo de vida | AcessÃvel por JS? | Enviado nas requests? | Quando usar |
|---|---|---|---|---|---|
localStorage | ~5MB | Persiste indefinidamente | Sim | Não | Tema, preferências, rascunho de formulário |
sessionStorage | ~5MB | Enquanto a aba estiver aberta | Sim | Não | Filtros temporários, wizard multi-step |
cookies | ~4KB cada | Configurável (Expires/Max-Age) | Sim (a menos que HttpOnly) | Sim, automaticamente | Autenticação (com HttpOnly + Secure + SameSite) |
// String — direto
localStorage.setItem('tema', 'dark');
const tema = localStorage.getItem('tema') ?? 'light';
// Objeto/array — sempre via JSON
localStorage.setItem('filtros', JSON.stringify({ ordem: 'preco', ativos: true }));
const filtros = JSON.parse(localStorage.getItem('filtros') ?? '{}');
// Remover / limpar
localStorage.removeItem('tema');
localStorage.clear(); // CUIDADO: limpa tudo do domÃnio
Qualquer JavaScript que rode na página pode ler todo o localStorage — incluindo extensões, scripts de terceiros e XSS. Nunca guarde senhas, tokens de API ou JWT ali. Para autenticação use cookies HttpOnly, que o JS não consegue acessar.
Outro detalhe importante: localStorage é sÃncrono. Cada chamada bloqueia a thread principal. Em volumes pequenos não tem impacto, mas se você precisar guardar centenas de KB ou objetos complexos, prefira IndexedDB, que é assÃncrono.
20. FormData — submissão sem reinventar parsing
Antes da API FormData, enviar um formulário via fetch exigia ler cada campo manualmente, montar um objeto, serializar. Com FormData, basta passar o elemento <form> e ela coleta tudo, incluindo arquivos.
const form = document.querySelector('#novo-produto');
form.addEventListener('submit', async (e) => {
e.preventDefault();
const data = new FormData(form);
// 1) Enviar como multipart/form-data (suporta upload de arquivo)
const r1 = await fetch('/produtos', { method: 'POST', body: data });
// 2) Converter para JSON (se a API espera JSON)
const obj = Object.fromEntries(data); // { nome: '...', preco: '...' }
const r2 = await fetch('/produtos', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(obj),
});
});
Object.fromEntries(formData) só funciona corretamente quando cada campo aparece uma única vez. Para campos repetidos (checkboxes múltiplos, name="tags"), use formData.getAll('tags') para obter um array.
21. Delegação de eventos
Em listas dinâmicas (cards, linhas de tabela, items adicionados via fetch), anexar um listener em cada item é desperdÃcio — e quebra quando novos items chegam. Delegação de eventos é o padrão correto: anexa um único listener no elemento pai, e dentro dele identifica o alvo via event.target.
// HTML: <ul class="produtos"> ... <li data-id="1">...<button class="btn-add">Add</button></li> ... </ul>
const lista = document.querySelector('.produtos');
lista.addEventListener('click', (e) => {
const btn = e.target.closest('.btn-add');
if (!btn) return; // clicou fora de um botão
const li = btn.closest('li');
const id = li.dataset.id;
adicionarAoCarrinho(Number(id));
});
O método Element.closest(seletor) sobe pela árvore até encontrar um ancestral que case com o seletor — é o que torna a delegação robusta mesmo quando o usuário clica em um filho do botão (um <span>, um Ãcone).
22. Exemplo end-to-end — listagem com filtro, busca e ordenação
Vamos juntar quase tudo desta aula em um cenário realista. O servidor renderiza, via EJS, uma página de produtos. O JavaScript do cliente acrescenta busca com debounce, ordenação dinâmica, filtro e persiste a preferência do usuário em localStorage.
<section class="products">
<header class="products__bar">
<input type="search" id="busca" aria-label="Buscar">
<select id="ordem">
<option value="nome">Nome</option>
<option value="preco">Preço</option>
</select>
</header>
<ul class="products__list">
<%# Renderizado pelo servidor a partir do banco %>
<% produtos.forEach(p => { %>
<li class="card" data-id="<%= p.id %>" data-nome="<%= p.nome %>" data-preco="<%= p.preco %>">
<h3 class="card__title"><%= p.nome %></h3>
<p class="card__price">R$ <%= p.preco.toFixed(2) %></p>
</li>
<% }) %>
</ul>
</section>
<script type="module" src="/js/products/list.js"></script>
import { debounce } from '../utils/debounce.js';
const $busca = document.querySelector('#busca');
const $ordem = document.querySelector('#ordem');
const $lista = document.querySelector('.products__list');
const CARDS = [...$lista.querySelectorAll('.card')];
// 1. carregar preferências persistidas
const prefs = JSON.parse(localStorage.getItem('produtos:prefs') ?? '{}');
if (prefs.busca) $busca.value = prefs.busca;
if (prefs.ordem) $ordem.value = prefs.ordem;
// 2. funções puras de filtro/ordenação
function filtrarEordenar() {
const termo = $busca.value.trim().toLowerCase();
const ordem = $ordem.value;
const visiveis = CARDS
.filter(c => c.dataset.nome.toLowerCase().includes(termo))
.sort((a, b) => {
if (ordem === 'preco') return Number(a.dataset.preco) - Number(b.dataset.preco);
return a.dataset.nome.localeCompare(b.dataset.nome);
});
// re-anexar na nova ordem; esconder os que não casaram
CARDS.forEach(c => { c.style.display = 'none'; });
visiveis.forEach(c => { c.style.display = ''; $lista.appendChild(c); });
localStorage.setItem('produtos:prefs', JSON.stringify({ busca: $busca.value, ordem }));
}
// 3. listeners — debounce na busca, imediato na ordenação
$busca.addEventListener('input', debounce(filtrarEordenar, 200));
$ordem.addEventListener('change', filtrarEordenar);
// 4. delegação para "adicionar ao carrinho"
$lista.addEventListener('click', (e) => {
const card = e.target.closest('.card');
if (!card) return;
const id = Number(card.dataset.id);
console.log('adicionar', id);
});
filtrarEordenar(); // aplicar prefs salvos no carregamento
Note como cada conceito da aula tem seu papel: ESM importa o utilitário; JSON + localStorage persistem; filter/sort são higher-order functions; o spread [...$lista.querySelectorAll(...)] converte NodeList em array; a delegação no $lista trata cliques de qualquer card. Sem framework. Sem build. Funciona.
Aprofundamento: por que filtrar no cliente faz sentido aqui?
▼Quando a lista cabe na memória do browser (até alguns milhares de items), filtrar e ordenar no cliente é instantâneo, não bate no servidor a cada tecla, e funciona sem rede. Para listas grandes (dezenas de milhares), o caminho é paginar no servidor — a busca volta a ser uma query SQL, talvez com Ãndice GIN em PostgreSQL para busca textual.
A regra prática: se uma resposta com 100% dos dados cabe em < 200KB de JSON, filtrar no cliente é melhor UX. Acima disso, paginar no servidor.
23. Checklist de estudo
Marque conforme dominar:
- ✓Sei declarar CSS Variables em
:roote sobrescrevê-las em[data-theme]. - ✓Sei diferenciar Flex e Grid e escolher um de cada para um caso real.
- ✓Sei usar
repeat(auto-fit, minmax(...))para grid responsivo sem media queries. - ✓Sei escrever CSS mobile-first com
@media (min-width). - ✓Uso
clamp()para tipografia fluida. - ✓Animo só
transformeopacity, e respeitoprefers-reduced-motion. - ✓Nomeio classes em BEM (
.bloco__elemento--modificador). - ✓Organizo o CSS em
base/layout/components/utilities/theme. - ✓Sei carregar JS com
type="module"e usarimport/export. - ✓Aplico destructuring, spread/rest e sei a diferença entre
||e??. - ✓Sei criar uma
classcom campo privado (#) e usá-la para encapsular um componente. - ✓Entendo o que é uma closure e consigo usar para criar estado privado.
- ✓Encadeio
filter/map/reduceem vez defor. - ✓Persisto preferências em
localStorage; sei por que NÃO guardar token lá. - ✓Envio formulários com
FormDatae converto para JSON quando preciso. - ✓Uso delegação de eventos em listas dinâmicas com
event.target.closest().
Referências
- MDN — Usando CSS Custom Properties — referência completa de variáveis em CSS
- MDN — CSS Flexible Box Layout — Flexbox em profundidade
- MDN — CSS Grid Layout — Grid de A a Z
- CSS-Tricks — Complete Guide to Grid — guia visual recomendado
- web.dev — Learn CSS — curso interativo do Google sobre CSS moderno
- BEM — Methodology — documentação oficial do BEM
- MDN — JavaScript Modules — ES Modules nativos no browser
- MDN — Window.localStorage — API de armazenamento local
- MDN — FormData — coleta e envio de formulários
- WCAG 2.1 — Quick Reference — diretrizes de acessibilidade web