Arrastar e Soltar
O aprendiz arrasta itens para zonas de destino correspondentes. Ideal para classificação, organização e associação de conceitos com categorias.
Experimente
Classifique: Avaliação Formativa ou Somativa?
Clique num item e depois na categoria, ou arraste. Pode arrastar de volta para a lista para mudar de ideia.
Formativa
Somativa
Código-fonte
Dados de exemplo
Cada item carrega a categoria correta; a lista é embaralhada só na exibição.
| 1 |
<?php
|
| 2 |
$instrucao = 'Arraste cada palavra para a categoria certa:';
|
| 3 |
$categorias = [
|
| 4 |
['id' => 'vertebrado', 'nome' => 'Vertebrado'],
|
| 5 |
['id' => 'invertebrado', 'nome' => 'Invertebrado'],
|
| 6 |
];
|
| 7 |
$itens = [
|
| 8 |
['id' => 'i1', 'texto' => 'Sapo', 'correta' => 'vertebrado'],
|
| 9 |
['id' => 'i2', 'texto' => 'Minhoca', 'correta' => 'invertebrado'],
|
| 10 |
['id' => 'i3', 'texto' => 'Tubarão', 'correta' => 'vertebrado'],
|
| 11 |
['id' => 'i4', 'texto' => 'Abelha', 'correta' => 'invertebrado'],
|
| 12 |
];
|
| 13 |
shuffle($itens);
|
| 14 |
?>
|
Estrutura da questão
Banco de itens arrastáveis acima, zonas de destino abaixo — cada item pode ir e voltar livremente até o aluno clicar em Verificar.
| 1 |
<div class="questao" data-tipo="arrastar-soltar">
|
| 2 |
|
| 3 |
<p class="questao-instrucao"><?= htmlspecialchars($instrucao) ?></p>
|
| 4 |
|
| 5 |
<div class="banco">
|
| 6 |
<?php foreach ($itens as $item): ?>
|
| 7 |
<div class="item" draggable="true" data-item-id="<?= $item['id'] ?>" data-correta="<?= $item['correta'] ?>">
|
| 8 |
<?= htmlspecialchars($item['texto']) ?>
|
| 9 |
</div>
|
| 10 |
<?php endforeach; ?>
|
| 11 |
</div>
|
| 12 |
|
| 13 |
<div class="zonas">
|
| 14 |
<?php foreach ($categorias as $cat): ?>
|
| 15 |
<div class="zona" data-zona-id="<?= $cat['id'] ?>" data-zona-label="<?= htmlspecialchars($cat['nome']) ?>">
|
| 16 |
<p class="zona-titulo"><?= htmlspecialchars($cat['nome']) ?></p>
|
| 17 |
<div class="zona-itens"></div>
|
| 18 |
</div>
|
| 19 |
<?php endforeach; ?>
|
| 20 |
</div>
|
Ações da questão
Verificar, o painel de resultado e o botão de reiniciar — os dois últimos nascem ocultos.
| 1 |
<button type="button" class="questao-verificar">Verificar</button>
|
| 2 |
|
| 3 |
<div class="questao-painel" hidden>
|
| 4 |
<p class="questao-painel-mensagem"></p>
|
| 5 |
</div>
|
| 6 |
|
| 7 |
<button type="button" class="questao-reset" hidden>Tentar novamente</button>
|
| 8 |
|
| 9 |
</div>
|
Banco de itens
Bandeja de onde os itens saem e para onde podem voltar.
| 1 |
.banco {
|
| 2 |
display: flex;
|
| 3 |
flex-wrap: wrap;
|
| 4 |
gap: 8px;
|
| 5 |
min-height: 44px;
|
| 6 |
margin-bottom: 20px;
|
| 7 |
padding: 12px;
|
| 8 |
border: 1px solid #cbd5e1;
|
| 9 |
border-radius: 12px;
|
| 10 |
background: #f8fafc;
|
| 11 |
}
|
Item arrastável
Cursor de "pegar", área grande o bastante pro toque.
| 1 |
.item {
|
| 2 |
padding: 8px 14px;
|
| 3 |
border: 1px solid #94a3b8;
|
| 4 |
border-radius: 8px;
|
| 5 |
background: #e2e8f0;
|
| 6 |
font-size: 14px;
|
| 7 |
cursor: grab;
|
| 8 |
user-select: none;
|
| 9 |
}
|
Estados de interação
Seleção neutra (aguardando clique na zona) e arraste em andamento.
| 1 |
.item.selecionada {
|
| 2 |
outline: 2px solid #6366f1;
|
| 3 |
}
|
| 4 |
|
| 5 |
.item.arrastando {
|
| 6 |
opacity: 0.5;
|
| 7 |
}
|
Zonas de destino
Borda tracejada até receber algo; realce ao arrastar por cima (o mesmo realce vale para o banco, quando o item volta).
| 1 |
.zonas {
|
| 2 |
display: grid;
|
| 3 |
grid-template-columns: 1fr 1fr;
|
| 4 |
gap: 12px;
|
| 5 |
margin-bottom: 20px;
|
| 6 |
}
|
| 7 |
|
| 8 |
.zona {
|
| 9 |
min-height: 80px;
|
| 10 |
padding: 12px;
|
| 11 |
border: 2px dashed #cbd5e1;
|
| 12 |
border-radius: 12px;
|
| 13 |
background: #f8fafc;
|
| 14 |
}
|
| 15 |
|
| 16 |
.zona-titulo {
|
| 17 |
margin: 0 0 8px;
|
| 18 |
font-size: 11px;
|
| 19 |
font-weight: 700;
|
| 20 |
letter-spacing: 0.05em;
|
| 21 |
text-transform: uppercase;
|
| 22 |
color: #64748b;
|
| 23 |
}
|
| 24 |
|
| 25 |
.zona-itens {
|
| 26 |
display: flex;
|
| 27 |
flex-wrap: wrap;
|
| 28 |
gap: 8px;
|
| 29 |
}
|
| 30 |
|
| 31 |
.zona.alvo,
|
| 32 |
.banco.alvo {
|
| 33 |
border-color: #818cf8;
|
| 34 |
}
|
| 35 |
|
| 36 |
.zona.preenchida {
|
| 37 |
border-style: solid;
|
| 38 |
}
|
Resultado da verificação
Verde para quem foi parar na categoria certa, vermelho para quem errou ou ficou esquecido no banco.
| 1 |
.item.correta {
|
| 2 |
border-color: #22c55e;
|
| 3 |
background: rgba(34, 197, 94, 0.15);
|
| 4 |
}
|
| 5 |
|
| 6 |
.item.errada {
|
| 7 |
border-color: #ef4444;
|
| 8 |
background: rgba(239, 68, 68, 0.15);
|
| 9 |
}
|
Ações
Botões de largura total, um de cada vez na tela.
| 1 |
.questao-verificar,
|
| 2 |
.questao-reset {
|
| 3 |
display: block;
|
| 4 |
width: 100%;
|
| 5 |
padding: 12px 16px;
|
| 6 |
border: none;
|
| 7 |
border-radius: 10px;
|
| 8 |
font: inherit;
|
| 9 |
font-weight: 600;
|
| 10 |
cursor: pointer;
|
| 11 |
}
|
| 12 |
|
| 13 |
.questao-verificar {
|
| 14 |
background: #4f46e5;
|
| 15 |
color: #fff;
|
| 16 |
}
|
| 17 |
|
| 18 |
.questao-verificar:disabled {
|
| 19 |
opacity: 0.6;
|
| 20 |
cursor: default;
|
| 21 |
}
|
| 22 |
|
| 23 |
.questao-reset {
|
| 24 |
background: #e5e7eb;
|
| 25 |
color: #1f2937;
|
| 26 |
}
|
Estado oculto
O atributo hidden nativo tem a mesma especificidade da regra de exibição acima — sem isto, "Tentar novamente" apareceria antes de verificar.
| 1 |
.questao-verificar[hidden],
|
| 2 |
.questao-reset[hidden] {
|
| 3 |
display: none;
|
| 4 |
}
|
Painel de resultado
Caixa de destaque com a contagem de acertos, abaixo das zonas.
| 1 |
.questao-painel {
|
| 2 |
margin: 16px 0;
|
| 3 |
padding: 16px;
|
| 4 |
border-radius: 12px;
|
| 5 |
background: #1e3a8a;
|
| 6 |
color: #dbeafe;
|
| 7 |
font-weight: 600;
|
| 8 |
}
|
Encontrar os elementos da questão
Localiza o banco de itens, as zonas de destino e as ações.
| 1 |
document.addEventListener('DOMContentLoaded', function () {
|
| 2 |
document.querySelectorAll('.questao[data-tipo="arrastar-soltar"]').forEach(function (questao) {
|
| 3 |
var banco = questao.querySelector('.banco');
|
| 4 |
var zonas = questao.querySelectorAll('.zona');
|
| 5 |
var verificar = questao.querySelector('.questao-verificar');
|
| 6 |
var painel = questao.querySelector('.questao-painel');
|
| 7 |
var reset = questao.querySelector('.questao-reset');
|
| 8 |
// O item selecionado por clique, aguardando o clique numa zona (ou no banco, para desfazer).
|
| 9 |
var selecionado = null;
|
| 10 |
|
| 11 |
function limparSelecao() {
|
| 12 |
if (selecionado) selecionado.classList.remove('selecionada');
|
| 13 |
selecionado = null;
|
| 14 |
}
|
Mover um item
Um único ponto de movimentação — troca o container do item (zona ou banco de volta) e mantém a marcação de "zona preenchida" em dia dos dois lados.
| 1 |
function atualizarZona(zona) {
|
| 2 |
zona.classList.toggle('preenchida', !!zona.querySelector('.item'));
|
| 3 |
}
|
| 4 |
|
| 5 |
function mover(item, destino) {
|
| 6 |
var zonaAnterior = item.closest('.zona');
|
| 7 |
if (destino.classList.contains('zona')) {
|
| 8 |
destino.querySelector('.zona-itens').appendChild(item);
|
| 9 |
atualizarZona(destino);
|
| 10 |
} else {
|
| 11 |
destino.appendChild(item);
|
| 12 |
}
|
| 13 |
if (zonaAnterior && zonaAnterior !== destino) atualizarZona(zonaAnterior);
|
| 14 |
item.classList.remove('correta', 'errada');
|
| 15 |
limparSelecao();
|
| 16 |
}
|
Selecionar e arrastar
Clicar seleciona (clicar de novo desfaz); o stopPropagation impede que o clique "vaze" para a zona ou o banco por baixo do item e mova ele sozinho de volta.
| 1 |
banco.querySelectorAll('.item').forEach(function (item) {
|
| 2 |
item.addEventListener('click', function (evento) {
|
| 3 |
evento.stopPropagation();
|
| 4 |
if (verificar.disabled) return;
|
| 5 |
if (selecionado === item) { limparSelecao(); return; }
|
| 6 |
limparSelecao();
|
| 7 |
selecionado = item;
|
| 8 |
item.classList.add('selecionada');
|
| 9 |
});
|
| 10 |
item.addEventListener('dragstart', function (evento) {
|
| 11 |
if (verificar.disabled) { evento.preventDefault(); return; }
|
| 12 |
selecionado = item;
|
| 13 |
item.classList.add('arrastando');
|
| 14 |
evento.dataTransfer.setData('text/plain', item.dataset.itemId);
|
| 15 |
});
|
| 16 |
item.addEventListener('dragend', function () {
|
| 17 |
item.classList.remove('arrastando');
|
| 18 |
limparSelecao();
|
| 19 |
});
|
| 20 |
});
|
Zonas de destino
Clique (com um item selecionado) ou soltar via arraste têm o mesmo efeito: mover o item pra dentro da zona.
| 1 |
zonas.forEach(function (zona) {
|
| 2 |
zona.addEventListener('click', function () { if (selecionado && !verificar.disabled) mover(selecionado, zona); });
|
| 3 |
zona.addEventListener('dragover', function (evento) { evento.preventDefault(); zona.classList.add('alvo'); });
|
| 4 |
zona.addEventListener('dragleave', function () { zona.classList.remove('alvo'); });
|
| 5 |
zona.addEventListener('drop', function (evento) {
|
| 6 |
evento.preventDefault();
|
| 7 |
zona.classList.remove('alvo');
|
| 8 |
var item = questao.querySelector('[data-item-id="' + evento.dataTransfer.getData('text/plain') + '"]');
|
| 9 |
if (item) mover(item, zona);
|
| 10 |
});
|
| 11 |
});
|
Devolver ao banco
O banco também é um alvo válido de arraste (ou clique) — devolver um item aqui é como o aluno desfaz uma escolha antes de verificar.
| 1 |
banco.addEventListener('click', function () { if (selecionado && !verificar.disabled) mover(selecionado, banco); });
|
| 2 |
banco.addEventListener('dragover', function (evento) { evento.preventDefault(); banco.classList.add('alvo'); });
|
| 3 |
banco.addEventListener('dragleave', function () { banco.classList.remove('alvo'); });
|
| 4 |
banco.addEventListener('drop', function (evento) {
|
| 5 |
evento.preventDefault();
|
| 6 |
banco.classList.remove('alvo');
|
| 7 |
var item = questao.querySelector('[data-item-id="' + evento.dataTransfer.getData('text/plain') + '"]');
|
| 8 |
if (item) mover(item, banco);
|
| 9 |
});
|
Verificar
Todo item conta — inclusive quem ficou esquecido no banco, que nunca é marcado como certo.
| 1 |
verificar.addEventListener('click', function () {
|
| 2 |
limparSelecao();
|
| 3 |
var todosItens = questao.querySelectorAll('.item');
|
| 4 |
var acertos = 0;
|
| 5 |
todosItens.forEach(function (item) {
|
| 6 |
var zona = item.closest('.zona');
|
| 7 |
var correto = !!zona && item.dataset.correta === zona.dataset.zonaId;
|
| 8 |
item.classList.add(correto ? 'correta' : 'errada');
|
| 9 |
if (correto) acertos++;
|
| 10 |
});
|
| 11 |
|
| 12 |
painel.querySelector('.questao-painel-mensagem').textContent = acertos === todosItens.length
|
| 13 |
? 'Correto! Todos os itens na categoria certa.'
|
| 14 |
: acertos + ' de ' + todosItens.length + ' corretos.';
|
| 15 |
painel.hidden = false;
|
| 16 |
|
| 17 |
verificar.disabled = true;
|
| 18 |
verificar.hidden = true;
|
| 19 |
reset.hidden = false;
|
| 20 |
});
|
Reiniciar
Devolve todo mundo ao banco e limpa os estados, inclusive de zonas que ficaram marcadas como preenchidas.
| 1 |
reset.addEventListener('click', function () {
|
| 2 |
questao.querySelectorAll('.item').forEach(function (item) {
|
| 3 |
item.classList.remove('selecionada', 'arrastando', 'correta', 'errada');
|
| 4 |
banco.appendChild(item);
|
| 5 |
});
|
| 6 |
zonas.forEach(function (zona) { zona.classList.remove('preenchida', 'alvo'); });
|
| 7 |
painel.hidden = true;
|
| 8 |
verificar.disabled = false;
|
| 9 |
verificar.hidden = false;
|
| 10 |
reset.hidden = true;
|
| 11 |
});
|