Skip to content

Interactions avancées

Maîtriser les interactions au-delà du simple clic. Le web permet des gestes riches : glisser-déposer, secouer, appuis longs, raccourcis clavier. Ces interactions créent des expériences plus immersives et personnelles.

Ce que vous allez apprendre :

  • Drag and drop
  • Détection de gestes (shake, swipe, long-press)
  • Événements clavier et combos
  • Inputs utilisateur variés

Glisser-déposer est essentiel pour manipuler des objets virtuels : trier des cartes, organiser un espace, dessiner un parcours.

<script>
let position = $state({ x: 100, y: 100 });
let isDragging = $state(false);
let offset = $state({ x: 0, y: 0 });
function handleMouseDown(event) {
isDragging = true;
// Calculer l'offset entre la position de la souris et le coin de l'élément
offset = {
x: event.clientX - position.x,
y: event.clientY - position.y
};
}
function handleMouseMove(event) {
if (!isDragging) return;
position = {
x: event.clientX - offset.x,
y: event.clientY - offset.y
};
}
function handleMouseUp() {
isDragging = false;
}
</script>
<svelte:window
on:mousemove={handleMouseMove}
on:mouseup={handleMouseUp}
/>
<div
class="card"
class:dragging={isDragging}
style="left: {position.x}px; top: {position.y}px;"
on:mousedown={handleMouseDown}
role="button"
tabindex="0"
>
Glissez-moi
</div>
<style>
.card {
position: absolute;
padding: 2rem;
background: white;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
cursor: grab;
user-select: none;
}
.card.dragging {
cursor: grabbing;
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
z-index: 100;
}
</style>

Points clés :

  • <svelte:window> : écoute les événements sur toute la page
  • Calculer l’offset pour que l’élément ne “saute” pas
  • user-select: none : empêche la sélection de texte pendant le drag
  • Curseur grab / grabbing pour feedback visuel
<script>
function handleTouchStart(event) {
const touch = event.touches[0];
isDragging = true;
offset = {
x: touch.clientX - position.x,
y: touch.clientY - position.y
};
}
function handleTouchMove(event) {
if (!isDragging) return;
event.preventDefault(); // Empêche le scroll
const touch = event.touches[0];
position = {
x: touch.clientX - offset.x,
y: touch.clientY - offset.y
};
}
function handleTouchEnd() {
isDragging = false;
}
</script>
<div
on:touchstart={handleTouchStart}
on:touchmove={handleTouchMove}
on:touchend={handleTouchEnd}
>
<!-- ... -->
</div>

Les gestes ajoutent une couche d’interactivité physique. Secouer pour réinitialiser, appui long pour révéler, swipe pour naviguer.

<script>
import { onMount } from 'svelte';
let shakeCount = $state(0);
let lastX = 0, lastY = 0, lastZ = 0;
const SHAKE_THRESHOLD = 15; // Sensibilité
onMount(() => {
if (!window.DeviceMotionEvent) {
console.warn('Accéléromètre non disponible');
return;
}
function handleMotion(event) {
const { x, y, z } = event.accelerationIncludingGravity;
// Calculer le changement d'accélération
const deltaX = Math.abs(x - lastX);
const deltaY = Math.abs(y - lastY);
const deltaZ = Math.abs(z - lastZ);
if (deltaX + deltaY + deltaZ > SHAKE_THRESHOLD) {
shakeCount++;
}
lastX = x;
lastY = y;
lastZ = z;
}
window.addEventListener('devicemotion', handleMotion);
return () => {
window.removeEventListener('devicemotion', handleMotion);
};
});
</script>
<p>Secousses détectées : {shakeCount}</p>

Usage narratif : Secouer pour révéler un secret, effacer un dessin, générer un événement aléatoire.

<script>
let pressTimer;
let isLongPressed = $state(false);
const LONG_PRESS_DURATION = 800; // ms
function handlePressStart() {
pressTimer = setTimeout(() => {
isLongPressed = true;
// Action à l'appui long
console.log('Appui long détecté');
}, LONG_PRESS_DURATION);
}
function handlePressEnd() {
clearTimeout(pressTimer);
if (!isLongPressed) {
// C'était un clic normal
console.log('Clic court');
}
isLongPressed = false;
}
</script>
<button
on:mousedown={handlePressStart}
on:mouseup={handlePressEnd}
on:mouseleave={handlePressEnd}
on:touchstart={handlePressStart}
on:touchend={handlePressEnd}
on:touchcancel={handlePressEnd}
>
Maintenez appuyé
</button>

Usage narratif : Révéler une information cachée, charger une action, écouter un son.

<script>
let touchStartX = $state(0);
let touchStartY = $state(0);
const SWIPE_THRESHOLD = 50; // Pixels minimum
function handleTouchStart(event) {
const touch = event.touches[0];
touchStartX = touch.clientX;
touchStartY = touch.clientY;
}
function handleTouchEnd(event) {
const touch = event.changedTouches[0];
const deltaX = touch.clientX - touchStartX;
const deltaY = touch.clientY - touchStartY;
// Détecter la direction dominante
if (Math.abs(deltaX) > Math.abs(deltaY)) {
// Swipe horizontal
if (Math.abs(deltaX) > SWIPE_THRESHOLD) {
if (deltaX > 0) {
console.log('Swipe droite');
} else {
console.log('Swipe gauche');
}
}
} else {
// Swipe vertical
if (Math.abs(deltaY) > SWIPE_THRESHOLD) {
if (deltaY > 0) {
console.log('Swipe bas');
} else {
console.log('Swipe haut');
}
}
}
}
</script>
<div
on:touchstart={handleTouchStart}
on:touchend={handleTouchEnd}
class="swipe-area"
>
Swipez dans n'importe quelle direction
</div>
<style>
.swipe-area {
width: 100%;
height: 300px;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
touch-action: pan-y; /* Autorise le scroll vertical */
}
</style>

Usage narratif : Navigation entre scènes, exploration spatiale, contrôle de timeline.

Le clavier offre des interactions rapides et précises. Utile pour des jeux, des raccourcis, ou des expériences poétiques basées sur la frappe.

<script>
import { onMount } from 'svelte';
let lastKey = $state('');
onMount(() => {
function handleKeyPress(event) {
lastKey = event.key;
}
window.addEventListener('keydown', handleKeyPress);
return () => {
window.removeEventListener('keydown', handleKeyPress);
};
});
</script>
<p>Dernière touche : {lastKey}</p>
<script>
function handleKeyDown(event) {
// Détection de combo : Ctrl + S
if (event.ctrlKey && event.key === 's') {
event.preventDefault(); // Empêche la sauvegarde du navigateur
console.log('Sauvegarde personnalisée');
}
// Détection de flèches
if (event.key === 'ArrowUp') {
console.log('Haut');
}
// Shift + clic pour une action alternative
if (event.shiftKey) {
console.log('Shift maintenu');
}
}
</script>
<svelte:window on:keydown={handleKeyDown} />
<script>
const KONAMI_CODE = ['ArrowUp', 'ArrowUp', 'ArrowDown', 'ArrowDown',
'ArrowLeft', 'ArrowRight', 'ArrowLeft', 'ArrowRight',
'b', 'a'];
let sequence = [];
function handleKeyDown(event) {
sequence = [...sequence, event.key].slice(-KONAMI_CODE.length);
if (JSON.stringify(sequence) === JSON.stringify(KONAMI_CODE)) {
console.log('🎉 Code secret activé !');
sequence = []; // Reset
}
}
</script>
<svelte:window on:keydown={handleKeyDown} />

Usage narratif : Easter eggs, codes secrets, contrôle de personnage, instrument musical.

Les formulaires ne sont pas que pour des sites corporate. Utilisés créativement, ils deviennent des outils de narration.

<script>
let userInput = $state('');
let wordCount = $derived(userInput.trim().split(/\s+/).filter(Boolean).length);
let mood = $derived(
userInput.includes('!') ? 'excité' :
userInput.includes('?') ? 'interrogatif' : 'neutre'
);
</script>
<textarea bind:value={userInput} placeholder="Écrivez quelque chose..."></textarea>
<p>Mots : {wordCount}</p>
<p>Ton détecté : {mood}</p>

Usage narratif : Journal intime, texte dont l’apparence change selon ce que vous écrivez.

<script>
let timeOfDay = $state(12); // 0-24
let skyColor = $derived(`hsl(${210 + timeOfDay * 3}, 70%, ${80 - timeOfDay * 2}%)`);
let period = $derived(
timeOfDay < 6 ? 'nuit' :
timeOfDay < 12 ? 'matin' :
timeOfDay < 18 ? 'après-midi' :
'soirée'
);
</script>
<div class="scene" style="background: {skyColor};">
<p>Il est {timeOfDay}h, c'est le {period}.</p>
</div>
<input
type="range"
min="0"
max="24"
bind:value={timeOfDay}
aria-label="Heure de la journée"
/>
<style>
.scene {
width: 100%;
height: 300px;
display: flex;
align-items: center;
justify-content: center;
transition: background 0.3s ease;
color: white;
font-size: 1.5rem;
}
input[type="range"] {
width: 100%;
margin-top: 1rem;
}
</style>

Usage narratif : Contrôle du temps, de l’humeur d’un personnage, de l’intensité d’une scène.

<script>
let selectedMood = $state(null);
const moods = [
{ id: 'happy', emoji: '😊', label: 'Joyeux' },
{ id: 'sad', emoji: '😢', label: 'Triste' },
{ id: 'angry', emoji: '😠', label: 'En colère' },
{ id: 'neutral', emoji: '😐', label: 'Neutre' }
];
</script>
<p>Comment vous sentez-vous ?</p>
<div class="mood-selector">
{#each moods as mood}
<button
class="mood-btn"
class:selected={selectedMood === mood.id}
on:click={() => selectedMood = mood.id}
>
<span class="emoji">{mood.emoji}</span>
<span class="label">{mood.label}</span>
</button>
{/each}
</div>
{#if selectedMood}
<p>Vous avez choisi : {moods.find(m => m.id === selectedMood)?.label}</p>
{/if}
<style>
.mood-selector {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
gap: 1rem;
margin: 2rem 0;
}
.mood-btn {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
padding: 1rem;
background: white;
border: 2px solid #ddd;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s ease;
}
.mood-btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.mood-btn.selected {
border-color: #4CAF50;
background: #f0f8f0;
}
.emoji {
font-size: 2rem;
}
.label {
font-size: 0.9rem;
}
</style>

Créez une interface qui combine plusieurs types d’interactions :

Contraintes :

  • Un élément déplaçable en drag & drop
  • Un slider qui influence l’apparence
  • Un raccourci clavier caché
  • Une zone swipe (mobile)
  • Un input texte qui change la narration

Exemple de concept :

  • Tableau de bord d’un vaisseau spatial
  • Studio d’enregistrement virtuel
  • Interface de création de créature
  • Console de contrôle temporel
  • ✅ Implémenter du drag & drop (souris + tactile)
  • ✅ Détecter des gestes (shake, swipe, long-press)
  • ✅ Écouter le clavier (touches, combos, séquences)
  • ✅ Utiliser les inputs comme outils narratifs
  • ✅ Combiner plusieurs interactions dans une expérience cohérente