Migração de Surface
Receitas para converter um micro frontend app existente de responsividade baseada em viewport para o contrato de surface.
Cada receita é rotulada:
| Rótulo | Significado |
|---|---|
| automática | Mecânica. A regra convertida significa a mesma coisa. |
| condicional | Segura apenas quando a precondição declarada vale. Verifique-a. |
| manual | Precisa de decisão humana; não existe uma única reescrita correta. |
| não convertível | Não existe forma equivalente em container query. Use host.surface ou mantenha o comportamento de viewport deliberadamente. |
Cada receita abaixo é uma técnica isolada. O repositório do Web Host mantém uma página executável combinando todas elas, executada por sua suíte de testes para que as receitas não apodreçam virando instruções erradas.
Receitas que dependem de trabalho ainda não entregue — variantes
surface-*do Tailwind, diagnósticos em tempo de build, rolagem mediada pelo host, hit testing — estão marcadas como ainda não entregue e descrevem apenas o que existe hoje.
Árvore de decisão: sobre o que é esta regra?
Antes de converter qualquer coisa, classifique a intenção. A maioria das migrações ruins são conversões corretamente executadas de regras que não deveriam ter sido convertidas.
A regra responde a quanto espaço ESTA PÁGINA tem?
├── sim → converter para @container wippy-surface (receitas 1-8)
├── não, ela responde à largura de UM COMPONENTE
│ → dê a esse componente seu próprio container (receita 22)
├── não, ela responde a uma PREFERÊNCIA do usuário/dispositivo
│ → deixe como @media (receita 13)
└── não, ela deliberadamente acompanha a JANELA DO NAVEGADOR
(um overlay real de janela inteira)
→ deixe como está, e documente o porquê
Se você não consegue dizer, deixe como está e revisite depois. Uma media query não convertida é meramente não portável; uma convertida errado está silenciosamente quebrada.
1. max-width → inline-size <= — automática
/* antes */ @media (max-width: 640px) { .nav { display: none } }
/* depois */ @container wippy-surface (max-width: 640px) { .nav { display: none } }
2. min-width → inline-size >= — automática
/* antes */ @media (min-width: 640px) { .sidebar { display: block } }
/* depois */ @container wippy-surface (min-width: 640px) { .sidebar { display: block } }
3. Um intervalo de largura delimitado — automática
/* antes */ @media (min-width: 640px) and (max-width: 1024px) { … }
/* depois */ @container wippy-surface (640px <= width <= 1024px) { … }
A sintaxe de intervalo é suportada em todos os motores que o contrato de surface
tem como alvo. A forma com and também funciona, se você preferir.
4. Múltiplos breakpoints, ordem de cascata preservada — automática
Container queries não mudam especificidade nem ordenação. Converta cada bloco e mantenha-os na mesma ordem de origem:
@container wippy-surface (min-width: 480px) { .grid { grid-template-columns: repeat(2, 1fr) } }
@container wippy-surface (min-width: 900px) { .grid { grid-template-columns: repeat(4, 1fr) } }
5. Queries de altura — condicional (apenas dimensionamento por container)
/* depois */ @container wippy-surface (min-height: 500px) { .tall-only { display: block } }
Precondição: a página é dimensionada por container. No dimensionamento por conteúdo, a altura da página é o próprio conteúdo dela, então queries de altura nunca casam. Declare a dependência para que ela falhe de forma ruidosa em vez de silenciosa:
{ "wippy": { "surface": { "contract": 1, "requirements": ["block-size"] } } }
6. Queries de aspect-ratio — condicional (apenas dimensionamento por container)
/* antes */ @media (min-aspect-ratio: 16/9) { … }
/* depois */ @container wippy-surface (min-aspect-ratio: 16/9) { … }
Mesma precondição da receita 5: aspect ratio precisa dos dois eixos.
7. Queries de orientação — condicional (apenas dimensionamento por container)
@container wippy-surface (orientation: landscape) descreve o formato do seu
painel, que geralmente é o que você quis dizer. Se você realmente quis dizer o
dispositivo, isso é uma media query — mantenha-a (receita 13).
8. Altura / aspect / orientação no dimensionamento por conteúdo — não convertível
Não há eixo de bloco para consultar. Reestruture para que o layout dependa do
eixo inline. Não finja com cqh — veja a receita 22.
Você não pode mudar o app para dimensionamento por container por conta própria: o
dimensionamento é definido por onde o Web Host renderiza o app, não por algo no
pacote dele. Se o layout realmente não puder funcionar sem o eixo de bloco,
declare requirements: ["block-size"] para que uma colocação dimensionada por
conteúdo seja recusada de imediato em vez de renderizar errado, e faça o app ser
renderizado em um contexto dimensionado por container (sua própria rota ou um
painel de layout). Veja "Container sizing and content sizing" em
Portabilidade de Surface.
9. Geometria aninhada dentro de uma media query ambiental — manual
/* antes */
@media (prefers-color-scheme: dark) and (min-width: 640px) { .panel { … } }
/* depois — separe: a preferência fica, a geometria se move */
@media (prefers-color-scheme: dark) {
@container wippy-surface (min-width: 640px) { .panel { … } }
}
Manual porque a ordem de aninhamento pode mudar quais declarações vencem quando as duas condições antes se combinavam em um único prelúdio. Reverifique o resultado.
10. Ramos OR com vírgula — manual
/* antes */ @media (max-width: 480px), (min-width: 1200px) { … }
Uma vírgula é OR. Dividi-la em dois blocos @container preserva o OR apenas se
os dois blocos forem, no restante, idênticos e adjacentes; se você acidentalmente
os aninhar, transformou OR em AND, o que não casa com nada. Duplique as
declarações em dois blocos irmãos:
@container wippy-surface (max-width: 480px) { … }
@container wippy-surface (min-width: 1200px) { … }
11. not, only, booleano complexo — manual
only é um artefato de media type e não tem equivalente em container — descarte.
not inverte a condição inteira nas duas sintaxes, mas a precedência difere assim
que você mistura and/or; parentetize explicitamente em vez de confiar no
agrupamento original.
12. screen / print combinados com geometria — manual
Tipos de mídia não têm forma em container. Mantenha o tipo como media query e aninhe a geometria dentro dele (como na receita 9). Layout de impressão, em particular, normalmente deve permanecer inteiramente baseado em viewport/página.
13. Preferências continuam sendo media queries — não convertível (e corretas assim)
prefers-color-scheme, prefers-contrast, prefers-reduced-motion,
forced-colors, hover, pointer, any-pointer. @container suporta apenas
features de tamanho. Converter essas produz uma regra que nunca casa.
14. Breakpoints em em — manual
@media (min-width: 40em) resolve em contra o tamanho de fonte inicial.
@container wippy-surface (min-width: 40em) resolve contra o tamanho de fonte
do container. Se eles diferem, seu breakpoint se move silenciosamente.
Converta para px, ou verifique antes o font-size computado do container.
15. Breakpoints em rem — manual
rem não é relativo à raiz dentro de @media. Condições de media query
resolvem tanto em quanto rem contra o tamanho de fonte inicial — o padrão do
navegador, independente de qualquer CSS do autor — enquanto @container os
resolve da maneira comum, contra o tamanho de fonte computado real da
raiz/container.
Então os dois já são diferentes no momento em que o tamanho de fonte da sua raiz
difere do padrão do navegador, sem nada mudar em tempo de execução. O reset comum
html { font-size: 62.5% } é suficiente para mover um breakpoint convertido de
640px para 400px.
"Nada muda o tamanho de fonte da raiz" não é, portanto, uma precondição
suficiente. Converta para px, exatamente como para em (receita 14), a menos
que o tamanho de fonte computado da raiz seja comprovadamente igual ao padrão do
navegador.
16. Fronteira entre viewport e content-box da barra de rolagem — condicional
100vw inclui a calha clássica da barra de rolagem. No motor de iframe, a
largura da surface é o content box da caixa de consulta dentro do documento
do app, então ela não inclui: em uma página com barra de rolagem de documento, o
valor convertido é mais estreito pela largura da barra, o que geralmente é a
correção que você queria (100vw causando overflow horizontal é um bug clássico).
O motor de fragment mede um wrapper no documento do host que a rolagem do conteúdo não estreita, então ele não aplica essa correção. Mesmo painel, mesmo conteúdo rolável, larguras diferindo por uma barra de rolagem. A condição desta receita é, portanto, em qual motor o app roda, e não apenas se o alinhamento é exato ao pixel.
17. Regras que miram html / body — manual
Uma container query nunca estiliza seu próprio container, e uma regra mirando
html ou body falha nos dois motores — por razões diferentes:
- Motor de iframe: o host envolve o conteúdo do seu body na caixa de surface,
então
htmlebodysão ancestrais do container de consulta. Uma regra@containernão consegue alcançar um ancestral. - Motor de fragment: a topologia oposta — a caixa de consulta é um wrapper do
documento do host acima do seu conteúdo — mas um seletor literal
bodyainda falha, porque o documento refletido é renomeado parawf-html/wf-body.
De qualquer forma, a correção é a mesma, e ela é segura em ambos os motores:
/* ✗ silenciosamente nunca casa */
@container wippy-surface (min-width: 640px) { body { display: flex } }
/* ✓ mova para sua própria raiz dentro da surface */
@container wippy-surface (min-width: 640px) { #app { display: flex } }
18. <picture><source media> e <link media> — não convertível
Seleção de recursos em nível de HTML não tem forma em container query. Ou
controle isso a partir do JS com host.surface.onChange, ou mova a direção de
arte para o CSS (background-image sob uma regra @container), onde o contrato
se aplica.
19. Geometria com matchMedia() → host.surface — automática
// antes
const mq = matchMedia('(min-width: 640px)')
mq.addEventListener('change', render)
// depois
const off = host.surface.onChange(s => render(s.width >= 640))
render(host.surface.snapshot.width >= 640)
// chame off() no teardown
Mantenha matchMedia para queries de preferência — apenas a geometria é que está
errada.
20. CSS em runtime, adopted stylesheets, CSS-in-JS — manual
Prefira emitir regras @container wippy-surface (...) e deixar o CSS responder.
Se você calcula pixels em JS, regenere a partir de onChange — um valor lido uma
única vez do snapshot fica congelado e dessincroniza no próximo resize. Nunca
emita você mesmo os quatro nomes reservados --wippy-surface-*, e nunca os
registre com @property / CSS.registerProperty() — o registro anula o sinal do
host de "eixo de bloco indisponível", então um app dimensionado por conteúdo se
reporta silenciosamente como dimensionado por container; uma declaração
descendente sombreia o valor herdado e desprende sua página da surface.
21. CSS empacotado de terceiros — manual
Normalmente você não pode editá-lo. Em ordem de preferência: configure a
biblioteca para aceitar um breakpoint/largura que você forneça a partir de
host.surface; envolva-a em seu próprio container e traduza; ou fixe a página no
motor de iframe (wippy.renderEngine: "iframe") e aceite o comportamento baseado
em janela. A varredura em tempo de build para encontrar esses casos
automaticamente ainda não foi entregue.
22. Containers aninhados e a armadilha do fallback de cq* — manual
Unidades de container resolvem contra o container mais próximo que tem o eixo de que precisam. Duas consequências:
.card { container-type: inline-size; } /* NÃO tem eixo de bloco */
.card .thing { block-size: 25cqh; } /* ✗ usa silenciosamente o small viewport */
cqh/cqb não geram erro quando nenhum container com eixo de bloco é
encontrado — eles recorrem ao small viewport e renderizam um número errado
plausível. Use var(--wippy-surface-height, <fallback>) quando quiser o eixo de
bloco da surface: ele é fixado na raiz, então um container mais próximo não pode
interceptá-lo, e ele cai visivelmente para o fallback quando indisponível.
Queries de componente são aditivas, não um substituto: wippy-surface continua se
referindo à área da página mesmo de dentro de um container aninhado.
Unidades de viewport
| Era | Use | Notas |
|---|---|---|
100vw |
var(--wippy-surface-width) |
content box; veja a receita 16 |
1vw / 37vw |
calc(var(--wippy-surface-width-unit) * 37) ou 37cqw |
a unidade é 1% |
100vh |
var(--wippy-surface-height) |
apenas dimensionamento por container |
1vh / 37vh |
calc(var(--wippy-surface-height-unit) * 37) |
apenas dimensionamento por container |
vmin |
min(var(--wippy-surface-width), var(--wippy-surface-height)) |
apenas dimensionamento por container — precisa dos dois eixos |
vmax |
max(var(--wippy-surface-width), var(--wippy-surface-height)) |
apenas dimensionamento por container |
vi / vb |
cqi / cqb, ou as variáveis físicas |
lógicas; as variáveis de surface são físicas |
sv* / lv* / dv* |
var(--wippy-surface-*) |
sem equivalentes separados. Elas descrevem estados do chrome do navegador que um painel não tem; a surface tem um único tamanho |
sv*/lv* são unidades CSS reais — elas não significam "surface".
Cálculos
/* antes */ block-size: calc(100vh - 4rem);
/* depois */ block-size: calc(var(--wippy-surface-height, 400px) - 4rem);
O fallback é deliberadamente fixo e obviamente errado em vez de 100vh — veja "Não esconda um contrato ausente atrás de um fallback" abaixo. Isso importa mais no eixo de bloco do que no inline: a altura é inválida em toda colocação dimensionada por conteúdo, não apenas onde o contrato está ausente, então um fallback de 100vh renderiza silenciosamente a altura da janela na primeira vez que o app é embutido.
min()/max()/clamp() convertem sem mudanças; substitua as unidades dentro deles.
Quando 100% é melhor que um valor de surface
Se um elemento deve preencher seu pai, use 100% ou w-full. Recorra a
--wippy-surface-width apenas quando você precisar especificamente da área da
página — tipicamente porque um ancestral é mais estreito e você quer escapar
dele. Fixar na raiz algo que deveria ser relativo ao pai é como um layout acaba
correto em uma profundidade de aninhamento e errado em outra.
Não esconda um contrato ausente atrás de um fallback
/* ✗ */ inline-size: var(--wippy-surface-width, 100vw);
Isso renderiza a largura da janela quando o contrato está ausente — exatamente o
bug que o contrato existe para prevenir, tornado invisível. Deixe falhar
visivelmente, ou escolha um fallback fixo que seja obviamente errado (400px)
para que seja notado.
Overlays
O contrato de surface não captura position: fixed — container-type
estabelece um contexto de formatação independente sem containment de layout,
então um container de consulta computa contain: none e não ancora nada. Isso é
verificado em Chromium, Firefox e WebKit. Overlays do PrimeVue e overlays fixos
feitos à mão continuam funcionando, então o posicionamento não precisa de
migração.
O dimensionamento deles precisa. Um overlay que deve cobrir a surface deve usar
inset: 0 — não 100vw/100vh, que medem a janela do navegador e estouram em
um host multi-painel, e não var(--wippy-surface-height), que é indisponível no
dimensionamento por conteúdo. Combine inset: 0 com position: absolute dentro
de uma raiz position: relative do próprio app, se ele precisar funcionar nos
dois motores; position: fixed só é correto no motor de iframe, pela razão
logo abaixo.
O que precisa de atenção é o motor, não o contrato: no motor Web Fragment,
position: fixed resolve contra a janela do host, não contra o seu painel.
Veja Motores de Renderização e fixe o app com
wippy.renderEngine: "iframe" se isso importar.
Posicionamento de overlay mediado pelo host e helpers de rolagem em
host.surface ainda não foram entregues.
Checklist
- Classifique cada regra (página / componente / preferência / janela deliberada).
- Converta geometria com intenção de página para
@container wippy-surface. - Substitua unidades de viewport pelas variáveis de surface.
- Mova qualquer regra que mirava
html/bodypara o seu próprio elemento raiz. - Reverifique breakpoints em
em. - Declare
requirementsse você depende do eixo de bloco. - Execute a página nos dois motores e nos dois dimensionamentos — container e
conteúdo são o que esta migração realmente aciona, e um app é dimensionado por
conteúdo sempre que é embutido em vez de roteado. Verifique em qual você está
com
host.surface.snapshot.sizing, e condicione o comportamento de eixo de bloco ahost.surface.supports('block-size').