A grande expansão do CSS
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.
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 |
|---|---|---|
| @floating-ui/dom | 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 |
| @popperjs/core | 14,1 kB | API mais antiga, mesmo problema fundamental: geometria pura em JavaScript que precisa ser reexecutada a cada evento de scroll e resize |
| tippy.js | 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.
.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 (Chrome 125), 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
- Position-anchor no MDN
- Introducing the CSS anchor positioning API
- Anchor positioning
- Basics of the CSS Anchor Positioning
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 |
|---|---|---|
| @radix-ui/react-popover | 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 |
| @headlessui/react (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
::backdroppara overlay visual- Gerenciamento de foco
- Top layer (popovers sempre são renderizados acima de outro conteúdo sem brigar com z-index)
<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
- Popover attribute no MDN
- Popover and dialog elements
- Introducing the Popover API
- Poppin’ In
- Open UI and the Popover API
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 |
|---|---|---|
| @radix-ui/react-dialog | 10,6 kB | Entrega seu próprio gerenciamento de foco e lógica de portal em cima do focus-trap |
| focus-trap (inclui tabbable) | 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.
<dialog id="my-dialog"> <p>This is a proper modal</p> <button onclick="this.closest('dialog').close()">Close</button></dialog>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
- Elemento dialog no MDN
- Elemento dialog
- The HTML Dialog Element: Your Native Solution for Accessible Modals and Popups
- How to Open and Close HTML Dialogs
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 |
|---|---|---|
| gsap 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 |
| gsap/ScrollTrigger | 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 |
| motion (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).
@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 benefícios de performance 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
- CSS scroll-driven animations no MDN
- animation-timeline no MDN
- A Practical Introduction to Scroll-Driven Animations with CSS scroll() and view()
- Scroll-driven animations case studies
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 |
|---|---|---|
| motion (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 |
| react-transition-group | 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.
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:
@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
- View Transition API no MDN.
- Smooth transitions with the View Transition API.
- Some practical examples of view transitions to elevate your UI.
- Dialog view transitions.
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 |
|---|---|---|
| react-select | 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 |
| @radix-ui/react-select | 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 |
| downshift | 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.
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
- Customizable select elements no MDN
- Abusing Customizable Selects
-
The
<select>element can now be customized with CSS - demo
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).
<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.
<!-- 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 Scoped Focusgroup Explainer é 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 |
|---|---|---|
| masonry-layout | 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 |
| isotope-layout | 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.
.grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: masonry;}Vale mencionar que este tema se conecta diretamente com a proposta Item Flow, 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 renomeada para Grid Lanes, 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
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.
textarea { field-sizing: content;}O Chrome disponibilizou isso em 2024. Algo pequeno, mas profundamente satisfatório. :)
Demos e recursos
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:
.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:
.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:
.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
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.
.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 |
|---|---|---|
| @popperjs/core | 14,1 kB | CSS Anchor Positioning |
| @floating-ui/dom | 8,1 kB | CSS Anchor Positioning |
| tippy.js | 14,1 kB | CSS Anchor Positioning + Popover API |
| @radix-ui/react-popover | 19,6 kB | Popover API + Anchor Positioning |
| @radix-ui/react-dialog | 10,6 kB | Elemento <dialog> |
| focus-trap (inclui tabbable) | 7,4 kB | Focus trap nativo do <dialog> |
| @headlessui/react (completo) | 59,1 kB | <dialog>, Popover API, <select> |
| gsap core | 26,6 kB | CSS @keyframes / animation |
| gsap/ScrollTrigger | 18,3 kB | CSS Scroll-Driven Animations |
| motion (anteriormente framer-motion) | 57,4 kB | View Transitions API + CSS Scroll-Driven Animations |
| react-transition-group | 4,0 kB | View Transitions API |
| react-select | 29,1 kB | <select> customizável |
| @radix-ui/react-select | 23,7 kB | <select> customizável |
| downshift | 14,3 kB | <select> customizável / <datalist> |
| masonry-layout | 6,7 kB | CSS Grid Lanes |
| isotope-layout | 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 proposta ao CSS Working Group 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. :)