JavaScript · Démo interactive

JS natif moderne
sans framework

Ce que le navigateur sait faire seul. Chaque section illustre une API native en situation réelle — zéro dépendance, zéro librairie.

01

Clipboard API

Accédez au presse-papier en lecture et écriture, de façon asynchrone et sécurisée. Fini le hack document.execCommand.

owlconcept.be/lab/modules/js/js-natif/
<div class="clip">
  <span class="clip__url" id="url">owlconcept.be/lab/</span>
  <button id="copyBtn">Copier le lien</button>
  <span class="clip__msg" id="msg"></span>
</div>
.clip { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
.clip__url {
  flex: 1; min-width: 200px; padding: .55rem .9rem;
  background: #0d1117; border: 1px solid #1e2d44; border-radius: 6px;
  font-family: monospace; font-size: .8rem; color: #c9d4e8;
}
.clip button {
  padding: .5rem 1rem; cursor: pointer; color: #c9d4e8;
  background: #1a2333; border: 1px solid #1e2d44; border-radius: 6px;
}
.clip button:hover { border-color: #c9952a; color: #c9952a; }
.clip__msg { font-family: monospace; font-size: .75rem; color: #45e88a; }
const btn = document.getElementById('copyBtn');
const url = document.getElementById('url');
const msg = document.getElementById('msg');

btn.addEventListener('click', async () => {
  try {
    await navigator.clipboard.writeText(url.textContent.trim());
    msg.textContent = '✓ Copié !';
  } catch {
    msg.textContent = '✕ Permission refusée.';
  }
  setTimeout(() => (msg.textContent = ''), 2000);
});
02

matchMedia()

Réagissez en JS aux mêmes conditions que vos media queries CSS. Détectez le thème système, la largeur de fenêtre ou les préférences de mouvement en temps réel.

prefers-color-scheme
prefers-reduced-motion
largeur ≥ 1024px
<div class="mm">
  <div class="mm__item" data-query="(prefers-color-scheme: dark)">
    <span class="mm__label">prefers-color-scheme</span>
    <span class="mm__val">—</span>
  </div>
  <div class="mm__item" data-query="(prefers-reduced-motion: reduce)">
    <span class="mm__label">prefers-reduced-motion</span>
    <span class="mm__val">—</span>
  </div>
  <div class="mm__item" data-query="(min-width: 1024px)">
    <span class="mm__label">largeur ≥ 1024px</span>
    <span class="mm__val">—</span>
  </div>
</div>
.mm { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
.mm__item {
  display: flex; flex-direction: column; align-items: center; gap: .5rem;
  padding: 1.25rem 1rem; text-align: center; background: #0d1117;
  border: 1px solid #1e2d44; border-radius: 6px; transition: border-color .3s;
}
.mm__label {
  font-family: monospace; font-size: .62rem; color: #8492ac;
  text-transform: uppercase; letter-spacing: .08em;
}
.mm__val { font-family: monospace; font-size: .8rem; color: #8492ac; transition: color .3s; }
.mm__item.is-match { border-color: #c9952a; }
.mm__item.is-match .mm__val { color: #c9952a; }
@media (max-width: 600px) { .mm { grid-template-columns: 1fr; } }
document.querySelectorAll('.mm__item').forEach((item) => {
  const mq  = window.matchMedia(item.dataset.query);
  const val = item.querySelector('.mm__val');

  const render = (matches) => {
    item.classList.toggle('is-match', matches);
    val.textContent = matches ? 'oui' : 'non';
  };

  render(mq.matches);                                     // état initial
  mq.addEventListener('change', (e) => render(e.matches)); // en temps réel
});
03

Custom Events

Créez vos propres événements pour faire communiquer des composants indépendants sans couplage direct — le pattern observateur en pur JS natif.

Émetteur
Récepteur
En attente…
<div class="evt">
  <button id="emit">Émettre un événement</button>
  <div class="evt__status" id="status">En attente…</div>
</div>
.evt { display: flex; align-items: center; gap: 1.5rem; flex-wrap: wrap; }
.evt button {
  padding: .5rem 1rem; cursor: pointer; color: #c9d4e8;
  background: #1a2333; border: 1px solid #1e2d44; border-radius: 6px;
}
.evt button:hover { border-color: #c9952a; color: #c9952a; }
.evt__status {
  padding: .55rem .9rem; font-size: .85rem; color: #8492ac;
  background: #0d1117; border: 1px solid #1e2d44; border-radius: 6px; transition: .3s;
}
.evt__status.is-active { color: #45e88a; border-color: #45e88a; }
// Écouter (n'importe où dans l'app, sans couplage direct)
document.addEventListener('owl:ping', (e) => {
  const status = document.getElementById('status');
  status.textContent = '✓ Reçu — ' + e.detail.message;
  status.classList.add('is-active');
  setTimeout(() => {
    status.textContent = 'En attente…';
    status.classList.remove('is-active');
  }, 2500);
});

// Émettre
document.getElementById('emit').addEventListener('click', () => {
  document.dispatchEvent(new CustomEvent('owl:ping', {
    detail: { message: 'événement dispatché ✓' }
  }));
});
04

Web Animations API

Animez des éléments directement en JS avec un contrôle précis : lecture, pause, vitesse, et sens. Le moteur d'animation CSS, piloté par code.

×1
<div class="waa">
  <div class="waa__orb" id="orb"></div>
  <div class="waa__controls">
    <button id="play">Jouer</button>
    <button id="pause">Pause</button>
    <button id="reverse">Inverser</button>
    <label class="waa__speed">Vitesse
      <input type="range" id="speed" min="0.1" max="3" step="0.1" value="1">
      <span id="speedVal">×1</span>
    </label>
  </div>
</div>
.waa { display: flex; flex-direction: column; gap: 1.5rem; overflow: hidden; }
.waa__orb {
  width: 48px; height: 48px; border-radius: 50%;
  background: radial-gradient(circle at 35% 35%, #4577e8, #c9952a);
  box-shadow: 0 0 24px rgba(69, 119, 232, .4);
}
.waa__controls { display: flex; align-items: center; gap: .75rem; flex-wrap: wrap; }
.waa button {
  padding: .35rem .75rem; cursor: pointer; color: #c9d4e8; font-size: .8rem;
  background: #1a2333; border: 1px solid #1e2d44; border-radius: 6px;
}
.waa button:hover { border-color: #c9952a; color: #c9952a; }
.waa__speed {
  display: flex; align-items: center; gap: .5rem; margin-left: auto;
  font-family: monospace; font-size: .72rem; color: #8492ac;
}
.waa__speed input { accent-color: #c9952a; }
const orb = document.getElementById('orb');

const anim = orb.animate([
  { transform: 'translateX(0) scale(1)',       opacity: 1 },
  { transform: 'translateX(220px) scale(1.3)', opacity: 0.75 }
], { duration: 1400, easing: 'ease-in-out', iterations: Infinity, direction: 'alternate' });

document.getElementById('play').onclick    = () => anim.play();
document.getElementById('pause').onclick   = () => anim.pause();
document.getElementById('reverse').onclick = () => anim.reverse();

document.getElementById('speed').oninput = (e) => {
  anim.playbackRate = +e.target.value;
  document.getElementById('speedVal').textContent = '×' + (+e.target.value).toFixed(1);
};
05

AbortController

Annulez des requêtes fetch en cours ou des écouteurs d'événements en une ligne. Essentiel pour les recherches en temps réel et les composants qui se démontent.

Prêt.
<div class="abort">
  <div class="abort__row">
    <button id="start">Lancer une requête (3s)</button>
    <button id="cancel" class="danger" disabled>Annuler</button>
  </div>
  <div class="abort__status" id="status">Prêt.</div>
</div>
.abort { display: flex; flex-direction: column; gap: 1rem; }
.abort__row { display: flex; gap: .75rem; flex-wrap: wrap; }
.abort button {
  padding: .5rem 1rem; cursor: pointer; color: #c9d4e8;
  background: #1a2333; border: 1px solid #1e2d44; border-radius: 6px;
}
.abort button:hover { border-color: #c9952a; color: #c9952a; }
.abort button.danger { color: #e84545; border-color: #e84545; }
.abort button:disabled { opacity: .35; pointer-events: none; }
.abort__status {
  padding: .55rem .9rem; font-family: monospace; font-size: .78rem; color: #8492ac;
  background: #0d1117; border: 1px solid #1e2d44; border-radius: 6px; transition: .3s;
}
.abort__status.is-loading { color: #4577e8; border-color: #4577e8; }
.abort__status.is-done    { color: #45e88a; border-color: #45e88a; }
.abort__status.is-aborted { color: #e84545; border-color: #e84545; }
const start  = document.getElementById('start');
const cancel = document.getElementById('cancel');
const status = document.getElementById('status');
let controller, timer;

start.addEventListener('click', () => {
  controller = new AbortController();
  start.disabled = true; cancel.disabled = false;
  status.className = 'abort__status is-loading';
  status.textContent = '⏳ Requête en cours… (simulation 3s)';

  // Ici, une vraie requête : fetch(url, { signal: controller.signal })
  timer = setTimeout(() => {
    status.className = 'abort__status is-done';
    status.textContent = '✓ Requête terminée.';
    start.disabled = false; cancel.disabled = true;
  }, 3000);

  // Quand le signal est abort → on annule (AbortError avec un vrai fetch)
  controller.signal.addEventListener('abort', () => {
    clearTimeout(timer);
    status.className = 'abort__status is-aborted';
    status.textContent = '✕ Requête annulée par AbortController.';
    start.disabled = false; cancel.disabled = true;
  });
});

cancel.addEventListener('click', () => controller?.abort());
06

Intersection Observer

Détecte quand un élément entre dans le champ de vision. Idéal pour les animations au défilement, le chargement différé et les statistiques de lecture — sans scroll event.

Scrollez dans le contenu — les cartes s'animent à l'entrée dans le champ de vision

Carte A
Carte B
Carte C
<div class="io">
  <button id="replay">Rejouer</button>
  <div class="io__grid">
    <div class="io__card">Carte A</div>
    <div class="io__card">Carte B</div>
    <div class="io__card">Carte C</div>
  </div>
</div>
.io button {
  margin-bottom: 1rem; padding: .35rem .75rem; cursor: pointer; color: #c9d4e8;
  background: #1a2333; border: 1px solid #1e2d44; border-radius: 6px; font-size: .8rem;
}
.io button:hover { border-color: #c9952a; color: #c9952a; }
.io__grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1rem; }
.io__card {
  padding: 1.5rem 1rem; text-align: center; font-family: monospace;
  font-size: .78rem; color: #8492ac; background: #0d1117;
  border: 1px solid #1e2d44; border-radius: 6px;
  opacity: 0; transform: translateY(16px);
  transition: opacity .5s, transform .5s, border-color .3s;
}
.io__card.visible { opacity: 1; transform: translateY(0); border-color: #c9952a; color: #e8edf7; }
@media (max-width: 600px) { .io__grid { grid-template-columns: 1fr; } }
const cards = document.querySelectorAll('.io__card');

const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      entry.target.classList.add('visible');
      observer.unobserve(entry.target);   // on ne déclenche qu'une fois
    }
  });
}, { threshold: 0.2 });

cards.forEach((card) => observer.observe(card));

// Rejouer : on retire la classe et on ré-observe
document.getElementById('replay').addEventListener('click', () => {
  cards.forEach((card) => {
    card.classList.remove('visible');
    observer.observe(card);
  });
});