microinteraction-design · git:20261023.7cf1aeb · 2026-10-23 · sha256 6074f320a5b981f2
microinteraction-design git:20261023.7cf1aebA
Immutable. This exact content never changes and is served at /api/v1/blob/6074f320a5b981f2.
---
name: microinteraction-design
description: 'Use when designing or reviewing control feedback, disclosure, removable presence, async status or native link selection in Terra Motion Skills.'
---
# Microinteraction design
Faça o controle comunicar seu estado real. Escolha somente os estados que a operação possui e mantenha o consumidor responsável pelos dados e ações.
**Catálogo desta atualização L3:** 11 recipes, 4 skills operacionais e 16 subpaths explícitos de recipes/CSS. Para layout e estados dinâmicos, consulte [layout-transitions](../layout-transitions/SKILL.md). L1/L2 foram publicados pela PR #1 (merge 491a934, CI/deploy 39881855054); esse registro é histórico e não valida L3. A integração e publicação de L3 exigem seus próprios gates. Pacotes npm não publicados; Release/tag v0.1.0 histórica e inalterada.
## Aplicação
Use para feedback de controles e disclosure de conteúdo em React. Escolha [Button Feedback](../../packages/motion-recipes/src/button-feedback/ButtonFeedback.tsx) para botão semântico com feedback de cor; escolha [Accordion Transition](../../packages/motion-recipes/src/accordion-transition/AccordionTransition.tsx) para conteúdo aberto/fechado controlado. Essas duas receitas originais não resolvem links, dialogs, menus com teclado composto nem uma operação assíncrona completa. As três adições L1 integradas na main pela PR #1 estão detalhadas ao final, incluindo navegação nativa e apresentação de estados.
Comece identificando: ação real, rótulo acessível, estados aplicáveis, estado controlado pelo consumidor e forma de cancelar ou reverter a interação. Preserve o contrato existente antes de alterar uma receita.
## Parâmetros
| Estado | Button Feedback | Accordion Transition |
| --------------------- | ---------------------------------- | ------------------------------------------- |
| hover | cor, somente em ponteiro com hover | dispensável |
| foco visível | outline nativo estilizado | outline no gatilho |
| press | cor, sem scale | clique/Enter/Espaço pede inversão de `open` |
| disabled | prop nativa, ação bloqueada | não existe prop disabled nesta API |
| aberto/fechado | não aplicável | consumidor controla `open` e `onOpenChange` |
| loading/success/error | não são gerenciados pela receita | não aplicáveis |
`ButtonFeedback` encaminha props nativas, `className`, conteúdo e ref; `reduceMotion` é `false` por padrão. Preserve o comportamento nativo de `type`: use explicitamente `type="button"` para ações que não submetem formulários. Duração CSS default 140 ms, via `--msl-motion-duration-button-feedback`; raio via `--msl-button-feedback-radius`.
`AccordionTransition` recebe `open`, `onOpenChange`, `trigger` e `children`; opcionais: `duration=180` ms (finita, não negativa), `reduceMotion=false`, `id` base e `className` no wrapper. Não invente props nem troque esse contrato por dados do showcase. IDs gerados são únicos; IDs explícitos precisam ser únicos no documento.
## Exemplo
```tsx
import { ButtonFeedback } from '@motion-skills-lab/motion-recipes/button-feedback';
import '@motion-skills-lab/motion-recipes/button-feedback.css';
export function ConfirmButton({
onConfirm,
disabled = false,
}: {
onConfirm: () => void;
disabled?: boolean;
}) {
return (
<ButtonFeedback type="button" disabled={disabled} onClick={onConfirm}>
Confirmar seleção
</ButtonFeedback>
);
}
```
Esse exemplo encaminha uma ação real; não anuncia sucesso. Button requer React/React DOM. Accordion requer também Motion. Importe CSS por subpath explícito; não dependa da aparência ou do runtime da vitrine. Para um exemplo completo do Accordion, consulte [motion-foundations](../motion-foundations/SKILL.md).
## Restrições
- Não use timers para simular sucesso, salvação, pagamento ou carregamento. Estados assíncronos pertencem à operação real do consumidor, com erro e cancelamento coerentes.
- Não aplique scale, deslocamento ou informação disponível apenas em hover ao botão operacional.
- Não coloque um botão/link dentro de `trigger`, que já está dentro de botão nativo. Conteúdo do painel pode incluir ações normais.
- Não bloqueie interação durante uma transição. `onOpenChange` solicita uma mudança; o componente só abre quando `open` muda.
- Se desmontar uma demo ou iniciar replay, cancele trabalhos pendentes antes de remover o componente. Não adicione motor ou recipe além do escopo autorizado. Os contratos adicionais locais L1 estão ao final.
## Acessibilidade
Valide nome acessível, foco visível, Enter/Espaço, Tab/Shift+Tab e toque sem hover. `disabled` nativo bloqueia ação; `aria-disabled` isolado não bloqueia eventos. O Accordion associa gatilho/painel por ARIA e mantém o painel fechado inerte e oculto da árvore acessível. Se o consumidor fechar um painel com foco interno, o foco retorna ao gatilho.
O sistema operacional sempre vence: a prop `reduceMotion` só acrescenta redução. A lógica e o texto precisam permanecer imediatos, sem aguardar callbacks de animação. Mudanças de preferência durante a sessão devem ser verificadas em browser.
## Validação
Execute os testes da receita alterada e um consumo pelos exports públicos. No browser, confirme foco/press, disabled, toque, abertura/fechamento/reversões, conteúdo oculto fora da tabulação, IDs únicos e preferência reduzida real. Em StrictMode, monte/desmonte e repita. Teste conteúdo longo e viewport de 320 px.
Apresente a matriz de estados aplicáveis, props usadas, comandos/resultados e pendências. Revise a solução se ela anunciar sucesso sem operação concluída, tornar conteúdo fechado navegável, perder foco ou reativar movimento contra a preferência do sistema. Auditoria axe complementa esses cenários; não os substitui.
## L1 — Presence, Async e Navigation (main e Pages; npm não publicado)
As três APIs abaixo estão integradas na main e demonstradas no Pages, sem bump de versão ou publicação npm. Os contratos anteriores permanecem válidos. Consulte os [contratos completos](../../docs/MOTION_CONTRACT.md) antes de editar.
| Recipe | Default | Fonte de verdade | Limite |
| -------------------- | ---------------------------------------------- | -------------------------- | -------------------------------------------------- |
| Presence Transition | 180 ms; entrada 6 px → 0, saída −4 px com fade | `present` | bloco único; sem modal, altura ou fila |
| Async State Feedback | 160 ms; apenas marcador decorativo | `state` + cinco `messages` | sem Promise, timers de sucesso ou operação interna |
| Navigation Indicator | 180 ms; linha decorativa | `value` + links `items` | horizontal LTR; sem tabs, router ou scroll spy |
### Procedimento operacional para o agente
1. Identifique se o conteúdo será removido, se um estado de operação será apresentado ou se uma navegação real terá indicação. Não use Presence para representar loading nem Async para executar a operação.
2. Leia os tipos por subpath público e importe o CSS correspondente. Motion é necessário para Presence/Navigation/Accordion; Button/Async funcionam sem Motion. Não importe o laboratório.
3. Presence: `present=true` inicial é estático; `false` não renderiza. Para fechamento com foco interno, forneça `returnFocusRef` externo, conectado, visível e focável. O foco sai antes de `inert`/`aria-hidden`. Alvo ausente, inválido ou desconectado causa blur/remoção imediata: é fallback de segurança, **não restauração acessível correta**. Corrija a integração do produto. `onExitComplete` só é válido depois da remoção/ref nula; reabertura, desmontagem e geração antiga cancelam o callback. Reverter antes da remoção conserva filhos; remover de fato perde seu estado. Não anime transform/opacity do wrapper externamente.
4. Async: forneça `idle`, `pending`, `success`, `error`, `cancelled` e mensagens não vazias. `announcement="off"` omite `role`, `aria-live` e `aria-atomic`; não use `role="status"` junto com off. Uma live region ancestral continua sob responsabilidade do produto. As cinco mensagens reservam a maior geometria via CSS; sem CSS, só o texto atual aparece. Teste fontes, traduções, quebra e 320 px/zoom nativo. Conteúdo muito longo pode aumentar a reserva; não fixe altura cortando texto.
5. Navigation: o produto controla `value`; links têm `id`, `href`, `label` e props nativas. Foco/hover não selecionam; clique navega de verdade. No laboratório, a fonte é URL/hash com `hashchange` e `popstate`, inclusive back/forward. Replay manual avança só um destino, sem sequência automática/timer. A medição usa link menos track, sem somar scrollLeft. Falta/falha de ResizeObserver, geometria zero/incerta ou RTL mantém indicação estática de `aria-current`; não esconda o link. A→B→C retargeta do estado visual corrente.
6. Em todos: duração finita, ≥0, em ms; zero é imediato. `reduceMotion=true` soma redução; false nunca supera o sistema. Alterar duração/redução ou desmontar deve encerrar recursos antigos. Não medir performance por duração de animação.
### Exemplo — bloco dispensável independente
```tsx
import { useRef, useState } from 'react';
import { PresenceTransition } from '@motion-skills-lab/motion-recipes/presence-transition';
import '@motion-skills-lab/motion-recipes/presence-transition.css';
export function RemovableNotice() {
const [present, setPresent] = useState(true);
const trigger = useRef<HTMLButtonElement>(null);
return (
<>
<button ref={trigger} type="button" onClick={() => setPresent((v) => !v)}>
Alternar aviso
</button>
<PresenceTransition present={present} returnFocusRef={trigger} duration={180}>
<p>Um contexto removível.</p>
<button type="button" onClick={() => setPresent(false)}>
Dispensar
</button>
</PresenceTransition>
</>
);
}
```
### Validação específica L1
Antes de entregar, teste SSR/hidratação real (não só string), StrictMode/unmount, duração zero/alterada, redução inicial/durante execução, foco interno com alvo válido e inválido, reversões, callback após DOM removido, textos longos/sem CSS, A→B→C e rail rolado/redimensionado. Compilar exemplos e passar axe não comprova anúncio em leitor de tela nem desempenho físico. Preserve logs de falhas e diferencie evidência atual de histórica.
### Exemplo — feedback controlado e links reais (L1)
```tsx
import {
AsyncStateFeedback,
type AsyncFeedbackState,
} from '@motion-skills-lab/motion-recipes/async-state-feedback';
import '@motion-skills-lab/motion-recipes/async-state-feedback.css';
import { NavigationIndicator } from '@motion-skills-lab/motion-recipes/navigation-indicator';
import '@motion-skills-lab/motion-recipes/navigation-indicator.css';
const messages = {
idle: 'Pronto',
pending: 'Processando',
success: 'Concluído',
error: 'Falhou',
cancelled: 'Cancelado',
};
export function ProductFeedback({ state }: { state: AsyncFeedbackState }) {
// O produto já possui a única live region da operação.
return (
<>
<AsyncStateFeedback state={state} messages={messages} announcement="off" />
<p role="status">{messages[state]}</p>
</>
);
}
export function DocumentNavigation({ section }: { section: string | null }) {
return (
<NavigationIndicator
aria-label="Neste documento"
currentType="location"
value={section}
items={[
{ id: 'overview', href: '#overview', label: 'Visão geral' },
{ id: 'details', href: '#details', label: 'Detalhes' },
]}
/>
);
}
```
No produto, conecte `section` à URL/hash e mantenha destinos reais com IDs `overview` e `details`. Assine `hashchange` e `popstate`, leia o estado inicial e remova listeners no unmount; o indicador não é roteador. Nunca marque sucesso apenas porque o marcador terminou sua transição. A demonstração do laboratório identifica explicitamente sua simulação controlada.
### Precisões de consumo L1
`onExitComplete?: () => void` não recebe argumentos; chama no máximo uma vez por saída válida, após remover o DOM e limpar a ref pública HTMLDivElement. Não sinaliza remoção de uma execução cancelada. Async deixa a concorrência para o produto: identifique a operação ativa e descarte resultados com ID antigo ou após desmontagem, mesmo quando não houver cancelamento físico. No laboratório, a fixture mostra esse descarte sem timer de sucesso.
Navigation: default `currentType='page'` (page/location), `duration=180` ms, `reduceMotion=false`, ref HTMLElement. `value=null` ou não encontrado mantém todos sem seleção; nome acessível, IDs/hrefs/labels não vazios e IDs únicos são exigidos. Hash desconhecido e algoritmo de próximo destino são regras da demo, não da API; na demo, desconhecido não seleciona e o avanço começa no primeiro item.