Criar um elemento arrastável

Adicione a ação de arrastar a um elemento flutuante, como um painel, uma paleta, uma barra de ferramentas, um inspetor ou uma caixa de diálogo. Arrastar não determina como o elemento é exibido, dispensado, empilhado ou focado.

Usar um identificador dedicado

Comece a arrastar de uma alça dedicada quando o elemento tiver texto, botões, entradas ou links selecionáveis. Isso mantém o conteúdo normal utilizável. Torne o identificador focalizável para o movimento do teclado e dê a ele um nome acessível que mencione a interação com as teclas de seta.

Não aplique role="application" ao identificador. Isso muda desnecessariamente a interação com a tecnologia adaptativa.

Aplique cursor: grab/grabbing, touch-action: none e user-select: none e -webkit-user-select: none à alça, não ao elemento arrastável inteiro, para que o texto fora dela permaneça selecionável e os gestos de toque mantenham o comportamento normal:

.drag-handle {
  cursor: grab;
  touch-action: none;
  -webkit-user-select: none;
  user-select: none;
}

.drag-handle.dragging {
  cursor: grabbing;
}

Preservar a posição e o alcance do ponteiro

Use um modelo de posicionamento relativo à janela de visualização, como position: fixed, ao fixar o movimento na janela de visualização.

  • Inicie uma ação de arrastar apenas para o botão do ponteiro principal (event.button === 0).
  • Registre o deslocamento do ponteiro em pointerdown e use-o em cada movimento para que o elemento não pule quando for arrastado para longe do canto.
  • Mantenha o arrasto ativo depois que o ponteiro sair da alça. Defina como padrão setPointerCapture() e limpe o estado de arrastar em lostpointercapture. Se o identificador puder ser removido ou renderizado novamente durante o arrasto, use os listeners pointermove, pointerup e pointercancel no nível do documento, já que a captura é perdida nesse caso.
  • Se quiser, aguarde cerca de 4 px de movimento antes de entrar no estado de arrastar, para que um clique na alça não acione o estilo de arrastar.

Com a captura de ponteiro, os listeners de movimento podem permanecer no identificador:

handle.addEventListener('pointerdown', (event) => {
  if (event.button !== 0 || event.target.closest('button, input, select, textarea, a')) return;

  const rect = draggable.getBoundingClientRect();
  offsetX = event.clientX - rect.left;
  offsetY = event.clientY - rect.top;
  handle.setPointerCapture(event.pointerId);
});

handle.addEventListener('pointermove', (event) => {
  if (!handle.hasPointerCapture(event.pointerId)) return;
  handle.classList.add('dragging');
  moveTo(event.clientX - offsetX, event.clientY - offsetY);
});

handle.addEventListener('lostpointercapture', () => {
  handle.classList.remove('dragging');
});

function moveTo(left, top) {
  const maxLeft = Math.max(0, window.innerWidth - draggable.offsetWidth);
  const maxTop = Math.max(0, window.innerHeight - draggable.offsetHeight);

  draggable.style.left = `${Math.max(0, Math.min(left, maxLeft))}px`;
  draggable.style.top = `${Math.max(0, Math.min(top, maxTop))}px`;
}

Fixe todas as posições do ponteiro e do teclado usando o mesmo auxiliar para que o elemento permaneça acessível. Fixar novamente a posição atual após mudanças na janela de visualização ou no tamanho do elemento. Para elementos com muitos layouts, considere fazer a translação durante o arrasto para evitar o layout por frame e, em seguida, confirme a posição final em left e top na liberação.

Oferecer movimento pelo teclado

Quando a alça estiver em foco, mova o elemento com as teclas de seta e chame preventDefault() para essas teclas. Use o mesmo assistente de posicionamento fixo que o movimento do ponteiro. Um incremento de 15px é um ponto de partida razoável. Adapte-o ao tamanho e às necessidades de precisão do componente.

Observação específica da caixa de diálogo

Um <dialog> ou [popover] pode usar esse mecanismo com o cabeçalho como identificador. Defina margin: 0 antes de atribuir left e top, porque as margens padrão do navegador interferem no posicionamento manual. Para saber mais sobre o comportamento de abertura, fechamento, foco e dispensa de caixas de diálogo e pop-overs, consulte Controle declarativo de caixas de diálogo e pop-overs e Caixa de diálogo de dispensa de controles da plataforma.

Estratégias de fallback

Quando o arrasto apenas reposiciona um componente, trate-o como aprimoramento progressivo: sem JavaScript, mantenha o elemento, o conteúdo e os controles visíveis, legíveis e funcionais em uma posição padrão sensata. Não exponha uma alça de arrastar não funcional: se a alça for um controle de fixação independente, oculte-a por padrão e revele-a quando o JavaScript for inicializado. Se um cabeçalho funcionar como a alça, anexe as funcionalidades de arrastar quando o JavaScript for inicializado.

Inclua -webkit-user-select: none antes de user-select: none para que a seleção de texto permaneça suprimida durante o arrasto no Safari.