# A grande expansão do CSS

> CSS agora faz nativamente o que Floating UI, GSAP, Framer Motion e react-select exigiam em JavaScript. Veja quanto isso economiza e o que ainda falta.

URL: https://dpw.dev/css/a-grande-expansao-do-css/  
Publicado em: 2026-04-09  
Categoria: css  
Tags: animacao-css, performance, layout  
Autor: Tárcio Zemel

Escolha um app web razoavelmente completo e faça uma auditoria no `node_modules`. Em algum lugar ali estará o Floating UI ou o Popper mantendo um tooltip ancorado a um botão. Um pacote Radix ou Headless UI gerenciando o focus trap de um modal. O GSAP ScrollTrigger ligando a posição de rolagem a uma animação. O React Select reconstruindo um `<select>` do zero porque o nativo não pode ser estilizado.

Antes de escrever uma única linha de código de produto, já se está enviando **centenas de kilobytes de JavaScript minificado e gzipado** apenas para lidar com padrões de UI que o navegador sempre deveria ter coberto.

> Artigo baseado em [The Great CSS Expansion](https://blog.gitbutler.com/the-great-css-expansion).

E esse peso vem com custos ocultos além dos kilobytes. Essas bibliotecas são escritas para o caso geral. O caso de uso específico é outro.

Mais cedo ou mais tarde, surge a aresta que elas não projetaram: um tooltip que precisa inverter em dois eixos, um modal que quebra dentro de um shadow DOM, uma animação de scroll que briga com o compositor do navegador, um select que não se integra de forma limpa com a biblioteca de formulários.

Abre-se a issue no GitHub. Escreve-se o workaround. Fixa-se a versão e torce-se para que a próxima major release não quebre tudo de novo.

O desenvolvimento web segue um ciclo familiar. Primeiro, cola-se uma solução com o que se tem (JavaScript, hacks de imagem, Flash etc.); depois, a plataforma amadurece, e o CSS ou HTML eventualmente torna esse mesmo workaround nativo.

Bordas arredondadas, fontes personalizadas, scroll suave, posicionamento sticky: tudo isso começou como hacks pesados em JavaScript antes do CSS transformá-los em uma única declaração.

Estamos em mais um desses momentos de transição. Uma nova onda de recursos CSS há muito solicitados está finalmente chegando, e muitos deles são explicitamente projetados para substituir padrões que costumavam exigir JavaScript.

Este artigo é um tour do que está sendo disponibilizado, o quê isso substitui, quanto JavaScript pode ser deletado e o quê ainda não foi resolvido.

## Anchor Positioning

Desde que tooltips, dropdowns, popovers e menus flutuantes existem na web, o JavaScript tem sido responsável por mantê-los atrelados aos seus elementos de gatilho. Ou se recorria ao Popper.js, ao Floating UI, ou se escrevia o próprio loop de `getBoundingClientRect`. O navegador não tinha conceito de "manter este elemento próximo daquele".

| Biblioteca                                                                                                              | Tamanho (min+gz) | Por que é problemática                                                                                                                                                                 |
| ----------------------------------------------------------------------------------------------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <a href="https://www.npmjs.com/package/@floating-ui/dom" target="_blank" rel="noopener noreferrer">@floating-ui/dom</a> | 8,1 kB           | A detecção de colisão (`flip`, `shift`, `autoPlacement`) precisa de ajuste fino por caso de uso; a biblioteca não tem conhecimento do contexto de overflow ou dos containers de scroll |
| <a href="https://www.npmjs.com/package/@popperjs/core" target="_blank" rel="noopener noreferrer">@popperjs/core</a>     | 14,1 kB          | API mais antiga, mesmo problema fundamental: geometria pura em JavaScript que precisa ser reexecutada a cada evento de scroll e resize                                                 |
| <a href="https://atomiks.github.io/tippyjs/" target="_blank" rel="noopener noreferrer">tippy.js</a>                     | 14,1 kB          | Construído sobre o @popperjs/core, então paga-se por ambos; sistema de temas opinativo que briga com os design tokens no momento em que se desvia dos padrões                          |

O CSS Anchor Positioning muda isso fundamentalmente. Declara-se um elemento como **âncora** com `anchor-name` e então posiciona-se outro elemento relativo a ele usando a função `anchor()` e `position-try`.

O navegador faz a matemática e também lida com overflow: se o tooltip ultrapassar a parte inferior da viewport, é possível definir posições de fallback e o navegador as tenta em ordem.

```css
.button {
  anchor-name: --my-button;
}

.tooltip {
  position: absolute;
  position-anchor: --my-button;
  top: anchor(bottom);
  left: anchor(left);
}
```

Sem JavaScript. Sem resize observers. Sem scroll listeners. O navegador mantém o elemento flutuante ancorado durante scroll, resize e mudanças de layout automaticamente.

O Chrome disponibilizou uma implementação abrangente em 2024 (<a href="https://developer.chrome.com/blog/anchor-positioning-api" target="_blank" rel="noopener noreferrer">Chrome 125</a>), e Firefox e Safari têm implementações parciais em andamento. Algumas propriedades individuais como `anchor-name` e a função básica `anchor()` já funcionam em todos os navegadores modernos; são os recursos mais avançados como `position-try` e `position-visibility` que ainda estão sendo lançados.

E mesmo que o suporte baseline ainda esteja em progresso, a demo abaixo deve funcionar na maioria dos navegadores mais recentes, então é possível testá-la agora mesmo.

### Demos e recursos

* <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/position-anchor" target="_blank" rel="noopener noreferrer">Position-anchor no MDN</a>
* <a href="https://developer.chrome.com/blog/anchor-positioning-api" target="_blank" rel="noopener noreferrer">Introducing the CSS anchor positioning API</a>
* <a href="https://web.dev/learn/css/anchor-positioning" target="_blank" rel="noopener noreferrer">Anchor positioning</a>
* <a href="https://ishadeed.com/article/anchor-positioning/" target="_blank" rel="noopener noreferrer">Basics of the CSS Anchor Positioning</a>

[Exemplo no CodePen](https://codepen.io/PavelLaptev/pen/019ce1d6-854e-7a3b-a40b-b637500415d6)

## Popover API

Anchor Positioning e a Popover API resolvem metades diferentes do mesmo problema.

O Anchor Positioning lida com **onde** um elemento flutuante aparece. A Popover API lida com **se** ele aparece — alternando visibilidade, light-dismiss, comportamento de teclado e acessibilidade.

Um tooltip completo precisa de ambos.

| Biblioteca                                                                                                                            | Tamanho (min+gz) | Por que é problemática                                                                                                                           |
| ------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| <a href="https://www.npmjs.com/package/@radix-ui/react-popover" target="_blank" rel="noopener noreferrer">@radix-ui/react-popover</a> | 19,6 kB          | Requer um Provider, tem sua própria renderização de portal e força o uso de sua API de compound component mesmo para um simples tooltip          |
| <a href="https://www.npmjs.com/package/@headlessui/react" target="_blank" rel="noopener noreferrer">@headlessui/react</a> (completo)  | 59,1 kB          | Não é tree-shakeable por componente; paga-se pelo pacote inteiro; lutar contra a abstração é comum quando o caso de uso não se encaixa no modelo |

Antes da Popover API, construir popovers, menus e overlays não-modais acessíveis significava gerenciar focus traps, estado `aria-expanded`, eventos de teclado e detecção de clique fora (tudo em JavaScript).

Bibliotecas como Headless UI e Radix existem em grande parte porque isso é extremamente tedioso de acertar.

O atributo HTML `popover` e seu companheiro `popovertarget` oferecem um popover nativo e acessível com um único atributo.

O navegador cuida de:

* Alternância de show/hide
* Light dismiss (clique fora fecha)
* Fechamento por teclado com Escape
* `::backdrop` para overlay visual
* Gerenciamento de foco
* Top layer (popovers sempre são renderizados acima de outro conteúdo sem brigar com z-index)

```html
<button popovertarget="menu">Open menu</button>

<div id="menu" popover>
  <p>I am a popover</p>
</div>
```

Isso já é baseline em Chrome, Firefox e Safari. Para muitos casos de uso, substitui todo o modelo de interação que bibliotecas de modal/dropdown vinham resolvendo.

A Popover API funciona bem para tooltips, menus dropdown, menus de contexto, toasts de notificação, callouts de onboarding e qualquer overlay não-modal que deve fechar quando o usuário clica fora.

Não é um substituto para diálogos modais (que será visto na próxima seção).

### Demos e recursos

* <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/popover" target="_blank" rel="noopener noreferrer">Popover attribute no MDN</a>
* <a href="https://web.dev/learn/css/popover-and-dialog" target="_blank" rel="noopener noreferrer">Popover and dialog elements</a>
* <a href="https://developer.chrome.com/blog/introducing-popover-api" target="_blank" rel="noopener noreferrer">Introducing the Popover API</a>
* <a href="https://css-tricks.com/poppin-in/" target="_blank" rel="noopener noreferrer">Poppin' In</a>
* <a href="https://utilitybend.com/blog/open-ui-and-the-popover-api/" target="_blank" rel="noopener noreferrer">Open UI and the Popover API</a>

[Exemplo no CodePen](https://codepen.io/PavelLaptev/pen/019ce20c-5d5b-736c-b883-13a023938b43)

## Elemento Dialog

Enquanto a Popover API é para overlays não-bloqueantes que fecham quando se clica fora, `<dialog>` é para experiências modais verdadeiras, o tipo que exige atenção antes que qualquer outra coisa possa acontecer.

A abordagem padrão em JavaScript envolvia uma div de backdrop, um container posicionado, definir manualmente `aria-modal`, prender o foco dentro, restaurar o foco ao fechar e impedir scroll no body.

| Biblioteca                                                                                                                                                                                                                   | Tamanho (min+gz) | Por que é problemática                                                                                                                              |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| <a href="https://www.npmjs.com/package/@radix-ui/react-dialog" target="_blank" rel="noopener noreferrer">@radix-ui/react-dialog</a>                                                                                          | 10,6 kB          | Entrega seu próprio gerenciamento de foco e lógica de portal em cima do `focus-trap`                                                                |
| <a href="https://www.npmjs.com/package/focus-trap" target="_blank" rel="noopener noreferrer">focus-trap</a> (inclui <a href="https://www.npmjs.com/package/tabbable" target="_blank" rel="noopener noreferrer">tabbable</a>) | 7,4 kB           | Enumera manualmente elementos focáveis e intercepta eventos Tab; bugs conhecidos com iframes e shadow DOM que o `<dialog>` nativo lida corretamente |

O elemento nativo `<dialog>` cuida de tudo isso. Combinado com o pseudo-elemento `::backdrop` e o método `.showModal()`, obtém-se um modal totalmente acessível com focus trap, usando um único elemento e cerca de três linhas de JavaScript para abri-lo.

```html
<dialog id="my-dialog">
  <p>This is a proper modal</p>
  <button onclick="this.closest('dialog').close()">Close</button>
</dialog>
```

```js
document.getElementById('my-dialog').showModal();
```

A parte do "modal trap" (onde Tab circula apenas dentro do diálogo e Escape o fecha) é tratada pelo navegador. Suporte baseline desde 2022.

### Popover vs. Dialog: qual é a diferença?

A diferença-chave se resume a se o resto da página permanece interativo:

| Recurso                         | Popover API             | Elemento Dialog                    |
| ------------------------------- | ----------------------- | ---------------------------------- |
| **Bloqueia interação de fundo** | Não                     | Sim                                |
| **Light dismiss (clique fora)** | Sim                     | Não                                |
| **Focus trap**                  | Não                     | Sim                                |
| **Trava de scroll**             | Não                     | Sim                                |
| **Usar para**                   | Tooltips, menus, toasts | Confirmações, formulários, alertas |
| **API**                         | Apenas HTML declarativo | Requer JS (`.showModal()`)         |

O modelo de controle reflete essa divisão. Um popover não precisa de JavaScript; `popovertarget` conecta o botão ao painel inteiramente em HTML.

Um dialog aberto com `.show()` é não-modal e praticamente inútil; o comportamento real vem de `.showModal()`, que é uma chamada JavaScript.

Ambos podem ser fechados com `Escape`, mas apenas o dialog prende o foco e impede a interação com o resto da página até ser dispensado.

### Demos e recursos

* <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog" target="_blank" rel="noopener noreferrer">Elemento dialog no MDN</a>
* <a href="https://web.dev/learn/html/dialog" target="_blank" rel="noopener noreferrer">Elemento dialog</a>
* <a href="https://dev.to/ilham-bouktir/the-html-dialog-element-your-native-solution-for-accessible-modals-and-popups-308p" target="_blank" rel="noopener noreferrer">The HTML Dialog Element: Your Native Solution for Accessible Modals and Popups</a>
* <a href="https://www.aleksandrhovhannisyan.com/blog/how-to-open-and-close-html-dialogs/" target="_blank" rel="noopener noreferrer">How to Open and Close HTML Dialogs</a>

[Exemplo no CodePen](https://codepen.io/PavelLaptev/pen/019ce242-f58d-712d-830a-a08368b0963b)

## Scroll-Driven Animations

Animações vinculadas a scroll costumavam significar uma coisa: um listener de scroll em JavaScript, frequentemente `requestAnimationFrame`, rastreando `window.scrollY` e atualizando custom properties CSS ou valores de transform a cada frame.

Em escala, isso se torna um problema de performance.

Bibliotecas como o GSAP ScrollTrigger existem em grande parte para tornar esse padrão gerenciável.

| Biblioteca                                                                                                                 | Tamanho (min+gz) | Por que é problemática                                                                                                                                                                                 |
| -------------------------------------------------------------------------------------------------------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| <a href="https://www.npmjs.com/package/gsap" target="_blank" rel="noopener noreferrer">gsap</a> core                       | 26,6 kB          | Necessário mesmo se o único recurso desejado for o ScrollTrigger; adiciona peso significativo para projetos que só precisam de animações acionadas por scroll                                          |
| <a href="https://gsap.com/docs/v3/Plugins/ScrollTrigger/" target="_blank" rel="noopener noreferrer">gsap/ScrollTrigger</a> | 18,3 kB          | Roda na thread principal; animações de scroll competem com tudo mais que o JS está fazendo; o timing de `start`/`end`/`scrub` requer iteração no navegador para acertar                                |
| <a href="https://motion.dev/" target="_blank" rel="noopener noreferrer">motion</a> (anteriormente framer-motion)           | 57,4 kB          | `useScroll` e `useTransform` são populares para efeitos vinculados a scroll, mas rodam em JavaScript e re-renderizam a cada tick de scroll — exatamente o que as CSS scroll-driven animations eliminam |

A especificação CSS Scroll-Driven Animations introduz `animation-timeline: scroll()` e `animation-timeline: view()`. Vincula-se uma animação CSS ao progresso de scroll diretamente (sem JavaScript envolvido).

```css
@keyframes fade-in {
  from { opacity: 0; transform: translateY(20px); }
  to { opacity: 1; transform: translateY(0); }
}

.card {
  animation: fade-in linear;
  animation-timeline: view();
  animation-range: entry 0% entry 30%;
}
```

O navegador executa isso na thread do compositor, o que significa que não bloqueia JavaScript e não engasga sob carga pesada — um ganho direto nos <a href="/ui-ux/deslocamento-layout-shift-cumulative-layout-shift-cls/">benefícios de performance</a> que já discutimos aqui no blog.

O Chrome já disponibilizou isso. O Firefox está em progresso. Para uso em produção, o fallback em JavaScript é simples, já que a animação é puramente decorativa.

### Demos e recursos

* <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Guides/Scroll-driven_animations" target="_blank" rel="noopener noreferrer">CSS scroll-driven animations no MDN</a>
* <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/animation-timeline" target="_blank" rel="noopener noreferrer">animation-timeline no MDN</a>
* <a href="https://tympanus.net/codrops/2024/01/17/a-practical-introduction-to-scroll-driven-animations-with-css-scroll-and-view/" target="_blank" rel="noopener noreferrer">A Practical Introduction to Scroll-Driven Animations with CSS scroll() and view()</a>
* <a href="https://web.dev/learn/css/scroll-animations" target="_blank" rel="noopener noreferrer">Scroll-driven animations case studies</a>

[Exemplo no CodePen](https://codepen.io/PavelLaptev/pen/019ce253-cbc2-735e-b9e6-6b1b166d4c0e)

## View Transitions

Transições de single-page application (aquelas em que navegar entre rotas anima a saída da página antiga e a entrada da nova) sempre exigiram JavaScript.

React tem o `react-transition-group`; Vue tem seu componente `<Transition>` e; soluções customizadas envolvem clonar elementos, posicionamento absoluto e coordenar timings de opacidade e transform manualmente.

| Biblioteca                                                                                                                          | Tamanho (min+gz) | Por que é problemática                                                                                                                                    |
| ----------------------------------------------------------------------------------------------------------------------------------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <a href="https://motion.dev/" target="_blank" rel="noopener noreferrer">motion</a> (anteriormente framer-motion)                    | 57,4 kB          | Apenas React, introduz complexidade de hidratação em apps SSR e traz seu próprio motor de animação mesmo quando o único objetivo são transições de página |
| <a href="https://www.npmjs.com/package/react-transition-group" target="_blank" rel="noopener noreferrer">react-transition-group</a> | 4,0 kB           | Apenas fornece alternância de classes CSS: morphing de elementos entre rotas ainda requer clonagem manual de DOM por cima                                 |

A View Transitions API permite que o navegador cuide disso. Envolve-se uma mudança de estado em `document.startViewTransition()` e o navegador automaticamente captura os estados antes e depois, faz cross-fade entre eles e permite customizar a transição com CSS.

```js
document.startViewTransition(() => {
  updateTheDom();
});
```

Para transições no mesmo documento, essa é toda a superfície da API. Para navegação entre documentos (carregamentos reais de página), é possível habilitá-la com uma única linha de CSS:

```css
@view-transition {
  navigation: auto;
}
```

View transitions nomeadas permitem animar elementos específicos entre páginas — como um card expandindo para uma página de detalhe — sem nenhum truque de clonagem de layout.

O Chrome já disponibilizou isso. Os roadmaps de Safari e Firefox estão ativos.

### Demos e recursos

* <a href="https://developer.mozilla.org/en-US/docs/Web/API/View_Transition_API" target="_blank" rel="noopener noreferrer">View Transition API no MDN</a>.
* <a href="https://developer.chrome.com/docs/web-platform/view-transitions/" target="_blank" rel="noopener noreferrer">Smooth transitions with the View Transition API</a>.
* <a href="https://piccalil.li/blog/some-practical-examples-of-view-transitions-to-elevate-your-ui/" target="_blank" rel="noopener noreferrer">Some practical examples of view transitions to elevate your UI</a>.
* <a href="https://medienbaecker.com/articles/dialog-view-transitions" target="_blank" rel="noopener noreferrer">Dialog view transitions</a>.

[Exemplo no CodePen](https://codepen.io/PavelLaptev/pen/019cf895-57da-782e-b966-e4f3865a0c31)

## Customização de Select

Dropdowns de select customizados são um dos elementos de UI mais duplicados nas webs.

Como o elemento nativo `<select>` era não-estilizável, cada design system construiu o seu do zero: um select nativo oculto para acessibilidade, uma div customizada agindo como gatilho visível, uma lista posicionada de opções, navegação por teclado, anúncios para screen readers.

São milhares de linhas de código para substituir um elemento de formulário.

| Biblioteca                                                                                                                          | Tamanho (min+gz) | Por que é problemática                                                                                                                                                                                     |
| ----------------------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <a href="https://www.npmjs.com/package/react-select" target="_blank" rel="noopener noreferrer">react-select</a>                     | 29,1 kB          | Notoriamente difícil de estilizar para um design system, as APIs `styles` e `classNames` são profundas e equipes rotineiramente acabam com centenas de linhas de sobrescritas                              |
| <a href="https://www.npmjs.com/package/@radix-ui/react-select" target="_blank" rel="noopener noreferrer">@radix-ui/react-select</a> | 23,7 kB          | Não se integra com submissão nativa de `<form>` sem inputs ocultos extras; envia sua própria navegação de teclado, gerenciamento ARIA e posicionamento de dropdown                                         |
| <a href="https://www.npmjs.com/package/downshift" target="_blank" rel="noopener noreferrer">downshift</a>                           | 14,3 kB          | Baixo nível o suficiente para ser flexível, mas ainda é necessário conectar todo comportamento manualmente: posicionamento, estilização e gerenciamento de estado ARIA são todos problema do desenvolvedor |

A proposta Customizable Select (anteriormente "Selectlist") oferece controle total via CSS sobre `<select>` e suas partes.

É possível estilizar o botão, o container do dropdown e elementos `<option>` individuais. Pode-se até colocar HTML arbitrário dentro das opções.

```css
select {
  appearance: base-select;
}

select::picker(select) {
  border: 1px solid #ccc;
  border-radius: 8px;
}
```

Esse é o recurso que a web precisava há mais de uma década!

O Chrome disponibilizou uma implementação inicial atrás de uma flag. A especificação ainda está evoluindo, mas a direção é clara.

### Demos e recursos

* <a href="https://developer.mozilla.org/en-US/docs/Learn_web_development/Extensions/Forms/Customizable_select" target="_blank" rel="noopener noreferrer">Customizable select elements no MDN</a>
* <a href="https://css-tricks.com/abusing-customizable-selects/" target="_blank" rel="noopener noreferrer">Abusing Customizable Selects</a>
* <a href="https://developer.chrome.com/blog/a-customizable-select" target="_blank" rel="noopener noreferrer">The `<select>` element can now be customized with CSS</a>
* <a href="https://codepen.io/t_afif/pen/PwGPJOB" target="_blank" rel="noopener noreferrer">demo</a>

[Exemplo no CodePen](https://codepen.io/PavelLaptev/pen/019cf8c8-f1ae-76be-a32a-913b2aa4c51e)

## Coisas menores que merecem destaque

### Focus Group

Navegação por setas dentro de widgets compostos (barras de ferramentas, listas de abas, grupos de radio, menus etc.) sempre significou escrever o mesmo boilerplate em JavaScript: anexar listeners de `keydown`, verificar `ArrowRight`/`ArrowLeft`/`ArrowUp`/`ArrowDown`, atualizar `tabindex` manualmente, lembrar o último elemento focado quando o usuário volta com Tab.

Toda biblioteca de UI entrega sua própria versão disso. O React tem padrões de `roving-tabindex`, o Angular CDK tem o `ListKeyManager`, o Fluent UI tem o `FocusZone`.

O atributo HTML `focusgroup`, proposto pelo Open UI, torna isso declarativo.

Adiciona-se ao container e o navegador cuida da navegação por setas entre seus filhos focáveis automaticamente (sem JavaScript).

```html
<div role="toolbar" focusgroup aria-label="Text Formatting">
  <button type="button" tabindex="-1">Bold</button>
  <button type="button" tabindex="-1">Italic</button>
  <button type="button" tabindex="-1">Underline</button>
</div>
```

O atributo aceita valores opcionais para ajustar o comportamento: `inline` ou `block` para restringir a navegação a um eixo, `wrap` para criar um loop e `no-memory` para sempre retornar o foco ao primeiro item em vez do último focado.

```html
<!-- Lista de abas: apenas esquerda/direita, com loop, sem memória para que a aba selecionada receba foco ao reentrar -->
<div role="tablist" focusgroup="inline wrap no-memory">
  <button role="tab" tabindex="0" aria-selected="true">Mac</button>
  <button role="tab" tabindex="-1" aria-selected="false">Windows</button>
  <button role="tab" tabindex="-1" aria-selected="false">Linux</button>
</div>
```

Focusgroups aninhados também funcionam: uma barra de menus horizontal e seus submenus verticais recebem cada um seu próprio eixo, então as setas ortogonais permanecem disponíveis para abrir/fechar menus.

O atributo `focusgroup` é atualmente uma proposta não-ativa do Open UI (nenhum navegador o disponibilizou ainda), mas o <a href="https://open-ui.org/components/scoped-focusgroup.explainer" target="_blank" rel="noopener noreferrer">Scoped Focusgroup Explainer</a> é uma versão ativamente mais restrita sendo direcionada para implementação.

O padrão que ele substitui é ubíquo o suficiente para que este recurso pareça inevitável.

### Masonry Layout (agora Grid Lanes)

| Biblioteca                                                                                                          | Tamanho (min+gz) | Por que é problemática                                                                                                                                                                                           |
| ------------------------------------------------------------------------------------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <a href="https://www.npmjs.com/package/masonry-layout" target="_blank" rel="noopener noreferrer">masonry-layout</a> | 6,7 kB           | Mede alturas após a pintura e aplica posicionamento absoluto; é preciso chamar `.layout()` manualmente após cada mudança no DOM, carregamento de imagem ou resize; transições CSS quebram sem workarounds extras |
| <a href="https://www.npmjs.com/package/isotope-layout" target="_blank" rel="noopener noreferrer">isotope-layout</a> | 9,1 kB           | Adiciona filtro e ordenação em cima do `masonry-layout` (do qual depende), roda inteiramente na thread principal e tem o mesmo problema de reflow pós-pintura                                                    |

Grids masonry estilo Pinterest (itens empacotados em colunas com alturas variáveis) têm sido um problema de layout em JavaScript desde sempre. O Masonry.js e o Isotope ainda são amplamente usados porque o CSS Grid não consegue fazer isso nativamente: os itens sempre começam de uma linha de grid estrita.

A proposta CSS Masonry adiciona `grid-template-rows: masonry` (e a variante de coluna). O navegador cuida do posicionamento dos itens para preencher lacunas, sem JavaScript.

> **Dica:** Se quiser se aprofundar mais em CSS Grid, confira <a href="/css/css-grid-o-jeito-certo/">CSS Grid: o jeito certo</a>.

```css
.grid {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  grid-template-rows: masonry;
}
```

Vale mencionar que este tema se conecta diretamente com a proposta <a href="/css/item-flow-sistema-layout-css/">Item Flow</a>, que busca unificar os sistemas de layout CSS.

O Firefox tem uma implementação experimental atrás de uma flag há anos. O debate da especificação entre uma abordagem baseada em grid versus um tipo `display: masonry` separado atrasou as coisas, mas tanto Chrome quanto Firefox estão ativamente trabalhando para disponibilizar algo.

A proposta foi desde então <a href="https://css-tricks.com/masonry-layout-is-now-grid-lanes/" target="_blank" rel="noopener noreferrer">renomeada para Grid Lanes</a>, refletindo a decisão de mantê-la dentro da especificação CSS Grid em vez de introduzir um tipo de display separado.

#### Demos e recursos

* <a href="https://webkit.org/blog/17660/introducing-css-grid-lanes/" target="_blank" rel="noopener noreferrer">Introducing CSS Grid Lanes</a>
* <a href="https://css-tricks.com/masonry-layout-is-now-grid-lanes/" target="_blank" rel="noopener noreferrer">Masonry Layout is Now grid-lanes</a> 

### Field Sizing

`field-sizing: content` faz com que elementos `<textarea>` e `<input>` cresçam automaticamente para caber seu conteúdo.

Aquele textarea auto-expansível implementado com um listener JavaScript de `input` e manipulação de `scrollHeight`? Acabou.

```css
textarea {
  field-sizing: content;
}
```

O Chrome disponibilizou isso em 2024. Algo pequeno, mas profundamente satisfatório. :)

#### Demos e recursos

* <a href="https://css-tricks.com/almanac/properties/f/field-sizing/" target="_blank" rel="noopener noreferrer">detalhamento do recurso</a>
* <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Properties/field-sizing" target="_blank" rel="noopener noreferrer">field-sizing no MDN</a>.

### Scroll State Queries

As CSS scroll-state queries, disponibilizadas no Chrome 133, estendem o modelo de container query para expor 3 informações de estado de scroll gerenciadas pelo navegador diretamente ao CSS (sem JavaScript).

**Stuck**: detectar se um elemento `position: sticky` está atualmente preso costumava exigir um elemento sentinela observado por `IntersectionObserver`.

Agora:

```css
.sticky-header {
  container-type: scroll-state;
  position: sticky;
  top: 0;

  > nav {
    @container scroll-state(stuck: top) {
      box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
    }
  }
}
```

**Snapped**:saber qual item é o elemento atualmente encaixado em um container de scroll snap costumava significar ouvir eventos `scrollsnapchange` em JavaScript e alternar classes.

Agora o item encaixado pode estilizar a si mesmo e seus filhos puramente com CSS:

```css
.carousel-item {
  container-type: scroll-state;
  scroll-snap-align: center;

  > * {
    @container not scroll-state(snapped: x) {
      opacity: 0.25;
    }
  }
}
```

**Scrollable**: mostrar sombras de scroll ou botões "voltar ao topo" apenas quando há conteúdo para rolar costumava exigir listeners de scroll JavaScript ou truques com IntersectionObserver. Agora:

```css
.scroll-container {
  container-type: scroll-state;
  overflow: auto;

  > .back-to-top {
    @container not scroll-state(scrollable: top) {
      display: none;
    }
  }
}
```

Todos os 3 substituem padrões que anteriormente precisavam de listeners de eventos JavaScript ou APIs de observer.

O Chrome 133 foi disponibilizado em janeiro de 2025. O suporte de Firefox e Safari está em andamento.

#### Demos e recursos

* <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/At-rules/@container#scroll-state" target="_blank" rel="noopener noreferrer">Scroll state queries no MDN</a>
* <a href="https://developer.chrome.com/blog/css-scroll-state-queries" target="_blank" rel="noopener noreferrer">CSS scroll-state()</a>
* <a href="https://css-tricks.com/scrollytelling-on-steroids-with-scroll-state-queries/" target="_blank" rel="noopener noreferrer">Scrollytelling on Steroids With Scroll-State Queries</a>

### CSS if() nativo

Custom properties CSS com fallbacks sempre foram uma aproximação de lógica condicional.

Apesar de sua sintaxe podre, a futura função `if()` no CSS permite valores genuinamente condicionais.

```css
.button {
  background: if(style(--variant: primary): blue; else: gray);
}
```

Combinada com as `@container style()` queries já existentes, isso começa a parecer lógica real em nível de componente no CSS.

Implementações experimentais estão em andamento.

## Quanto é possível economizar de fato?

Os números acima não são hipotéticos.

Aqui está quanto as bibliotecas sendo substituídas realmente custam, e o que o CSS nativo permite deletar:

| Biblioteca                                                                                                                                                                                                                   | Min + gzip   | Substituída por                                     |
| ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------- |
| <a href="https://www.npmjs.com/package/@popperjs/core" target="_blank" rel="noopener noreferrer">@popperjs/core</a>                                                                                                          | 14,1 kB      | CSS Anchor Positioning                              |
| <a href="https://www.npmjs.com/package/@floating-ui/dom" target="_blank" rel="noopener noreferrer">@floating-ui/dom</a>                                                                                                      | 8,1 kB       | CSS Anchor Positioning                              |
| <a href="https://atomiks.github.io/tippyjs/" target="_blank" rel="noopener noreferrer">tippy.js</a>                                                                                                                          | 14,1 kB      | CSS Anchor Positioning + Popover API                |
| <a href="https://www.npmjs.com/package/@radix-ui/react-popover" target="_blank" rel="noopener noreferrer">@radix-ui/react-popover</a>                                                                                        | 19,6 kB      | Popover API + Anchor Positioning                    |
| <a href="https://www.npmjs.com/package/@radix-ui/react-dialog" target="_blank" rel="noopener noreferrer">@radix-ui/react-dialog</a>                                                                                          | 10,6 kB      | Elemento `<dialog>`                                 |
| <a href="https://www.npmjs.com/package/focus-trap" target="_blank" rel="noopener noreferrer">focus-trap</a> (inclui <a href="https://www.npmjs.com/package/tabbable" target="_blank" rel="noopener noreferrer">tabbable</a>) | 7,4 kB       | Focus trap nativo do `<dialog>`                     |
| <a href="https://www.npmjs.com/package/@headlessui/react" target="_blank" rel="noopener noreferrer">@headlessui/react</a> (completo)                                                                                         | 59,1 kB      | `<dialog>`, Popover API, `<select>`                 |
| <a href="https://www.npmjs.com/package/gsap" target="_blank" rel="noopener noreferrer">gsap</a> core                                                                                                                         | 26,6 kB      | CSS `@keyframes` / `animation`                      |
| <a href="https://gsap.com/docs/v3/Plugins/ScrollTrigger/" target="_blank" rel="noopener noreferrer">gsap/ScrollTrigger</a>                                                                                                   | 18,3 kB      | CSS Scroll-Driven Animations                        |
| <a href="https://motion.dev/" target="_blank" rel="noopener noreferrer">motion</a> (anteriormente framer-motion)                                                                                                             | 57,4 kB      | View Transitions API + CSS Scroll-Driven Animations |
| <a href="https://www.npmjs.com/package/react-transition-group" target="_blank" rel="noopener noreferrer">react-transition-group</a>                                                                                          | 4,0 kB       | View Transitions API                                |
| <a href="https://www.npmjs.com/package/react-select" target="_blank" rel="noopener noreferrer">react-select</a>                                                                                                              | 29,1 kB      | `<select>` customizável                             |
| <a href="https://www.npmjs.com/package/@radix-ui/react-select" target="_blank" rel="noopener noreferrer">@radix-ui/react-select</a>                                                                                          | 23,7 kB      | `<select>` customizável                             |
| <a href="https://www.npmjs.com/package/downshift" target="_blank" rel="noopener noreferrer">downshift</a>                                                                                                                    | 14,3 kB      | `<select>` customizável / `<datalist>`              |
| <a href="https://www.npmjs.com/package/masonry-layout" target="_blank" rel="noopener noreferrer">masonry-layout</a>                                                                                                          | 6,7 kB       | CSS Grid Lanes                                      |
| <a href="https://www.npmjs.com/package/isotope-layout" target="_blank" rel="noopener noreferrer">isotope-layout</a>                                                                                                          | 9,1 kB       | CSS Grid Lanes                                      |
| **Total**                                                                                                                                                                                                                    | **\~322 kB** | —                                                   |

Considerando todos os tamanhos minificados + gzipados.

### Cenário conservador

Um app de conteúdo típico que usa Floating UI para um tooltip, Radix Dialog para um modal e GSAP ScrollTrigger para animações de scroll: são aproximadamente **44 kB a menos** (`@floating-ui/dom` 8,1 + `@radix-ui/react-dialog` 10,6 + `focus-trap` 7,4 + `gsap/ScrollTrigger` 18,3), sem contar o core do GSAP que talvez possa ser removido inteiramente.

### Cenário agressivo

Uma SPA pesada em design system que usa Headless UI em toda parte, Motion (anteriormente Framer Motion) para transições de página e react-select para um dropdown customizado: substituir apenas esses três remove **\~146 kB** de JavaScript do bundle.

### Além dos kilobytes

Tamanho de bundle é a parte visível do custo. A parte menos visível é o **tempo de parse** e **execução**. Cada kilobyte de JavaScript precisa ser baixado, parseado e executado antes de poder fazer qualquer coisa.

Em um dispositivo Android médio, 100 kB de JavaScript leva mensuravelmente mais tempo para executar do que 100 kB de CSS, porque CSS é parseado em uma thread separada e é mais barato de aplicar.

Bibliotecas que rodam na thread principal (GSAP ScrollTrigger, Masonry.js, Isotope) bloqueiam tudo mais enquanto trabalham.

Isso aparece diretamente nos Core Web Vitals:

* **INP (Interaction to Next Paint)** melhora quando há menos JavaScript competindo pela thread principal durante as interações do usuário. Substituir scroll listeners e resize observers por equivalentes CSS nativos reduz a contenção da thread principal.
* **LCP (Largest Contentful Paint)** pode melhorar quando menos JavaScript bloqueante de renderização atrasa a primeira pintura significativa — particularmente relevante quando essas bibliotecas são importadas de forma eager.
* **CLS (Cumulative Layout Shift)** é reduzido quando cálculos de layout acontecem nativamente no motor de layout do navegador em vez de em JavaScript que roda após a pintura. Masonry.js e Isotope são fontes comuns de CLS porque sua passada de posicionamento absoluto acontece depois que o navegador já fez o layout da página.

Há também o custo de manutenção: conflitos de peer dependency quando uma biblioteca requer uma versão específica do React, breaking changes em major releases, issues no GitHub para o caso extremo que se aplica ao projeto e a mais ninguém.

## O que o CSS ainda não resolveu

Apesar de todo o terreno que o CSS está cobrindo, há 2 categorias de interação que permanecem firmemente em território JavaScript.

### Drag and Drop

O drag and drop nativo do HTML (`draggable`, `ondragover`, `ondrop`) está tecnicamente disponível, mas é notoriamente não suficiente, com suporte pobre a touch, controle limitado sobre imagens de arraste, comportamento inconsistente entre navegadores. Toda implementação séria de drag-and-drop usa uma biblioteca JavaScript: dnd-kit, react-beautiful-dnd, SortableJS.

Não há proposta CSS ou HTML no horizonte que tornaria isso declarativo. Drag and drop envolve rastreamento complexo de gestos, hit testing, scroll durante arraste e semântica de acessibilidade que são genuinamente difíceis de abstrair. Este permanecerá em JavaScript no futuro previsível.

### Overlay Scrollbars

Overlay scrollbars — o tipo que flutua sobre o conteúdo e desaparece quando não está em uso, como as scrollbars de trackpad do macOS — não podem ser solicitadas com CSS. 

O CSS oferece `scrollbar-color` e `scrollbar-width` (ambos Baseline 2024/2025) para estilizar o thumb e a trilha, e `scrollbar-gutter` para gerenciar espaço de layout. Mas nenhum desses controla *onde* a scrollbar é renderizada em relação ao conteúdo.

No Windows, o Chrome usa scrollbars fixas que consomem espaço de layout independentemente de como são estilizadas.

A propriedade `scrollbar-style: overlay` foi <a href="https://github.com/w3c/csswg-drafts/issues/13218" target="_blank" rel="noopener noreferrer">proposta ao CSS Working Group</a> para preencher essa lacuna permitiria que autores solicitassem comportamento overlay mantendo a scrollbar visível e acessível, ao contrário de `scrollbar-width: none` que a esconde inteiramente.

A Microsoft está contribuindo com suporte a overlay scrollbar estilo Fluent para o Chromium atrás de uma flag. Mas ainda não há resolução de especificação nem implementação disponibilizada.

Até que isso aconteça, qualquer coisa que exija comportamento overlay verdadeiro no Windows permanece em JavaScript.

## O padrão continua

A Web está evoluindo de maneira rápida.

Fontes customizadas via image sprites, depois `@font-face`; bordas arredondadas via imagens de background, depois `border-radius`; scroll suave via JavaScript, depois `scroll-behavior`; headers sticky via hacks de `position: fixed`, depois `position: sticky`.

Os recursos acima são a geração atual desse mesmo ciclo de catch-up.

Alguns já estão sendo disponibilizados, alguns estão disponíveis através de flags, alguns ainda em debate de especificação.

Mas a direção é clara: se componentes de UI estão sendo construídos do zero em JavaScript porque o CSS não dava conta, vale checar de novo.

E lembre-se: é apenas o começo. :)

---

Fonte: [dpw - desenvolvimento para web](https://dpw.dev/) (pt-BR).
Guia para agentes: https://dpw.dev/llms.txt
Índice completo: https://dpw.dev/sitemap-index.xml
Feed: https://dpw.dev/rss.xml
