📌 Custom draggable alert futuristico con HTML/CSS/JS

Costruiamo un alert personalizzato futuristico (glass + neon), trascinabile anche su mobile grazie ai Pointer Events. Chiusura essenziale: solo OK ed ESC, con vincoli per restare sempre dentro la viewport.

Finestra di avviso futuristica semitrasparente con bordi al neon, etichetta «Info» e pulsante «OK».

Autore: Redazione · Creato: 04/03/2026 22:05

Custom draggable alert futuristico con HTML, CSS e JavaScript

Gli alert() nativi sono comodi ma grezzi: bloccano l’interazione, non sono personalizzabili e spezzano il flusso di una UI moderna. In questo tutorial costruiremo un alert personalizzato con stile futuristico (glass + glow), trascinabile con mouse e touch, e con un comportamento semplice e controllato.

Scelte di progetto:

• Alert non bloccante (non “congela” la pagina)
• Sempre visibile durante lo scroll (position: fixed)
• Trascinabile anche su mobile (Pointer Events)
• Non può uscire dallo schermo (vincoli viewport)
• Chiusura essenziale: solo OK e ESC (niente pulsante X)

Struttura del progetto

Crea una cartella (esempio): /assets/demos/draggable-alert/

Inserisci tre file:

• index.php
• styles.css
• script.js

1) HTML completo (index.php)

Questo file contiene una mini pagina demo con pulsanti per mostrare alert di tipi diversi e una sezione alta per testare lo scroll. L’alert ha una barra superiore che funge da “handle” per il trascinamento e un solo pulsante di chiusura: OK.

<!DOCTYPE html>
<html lang="it">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>Custom Draggable Alert — Futuristic UI</title>
  <link rel="stylesheet" href="styles.css" />
</head>

<body style="margin:0; min-height:100vh; background: radial-gradient(1200px 700px at 20% 10%, #101a3a 0%, #05070d 55%, #04050a 100%); color: rgba(255,255,255,.92); font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;">

  <div style="padding:18px; display:flex; gap:12px; align-items:center; flex-wrap:wrap;">
    <button class="neo-btn" id="btnDemo">Open Alert</button>
    <button class="neo-btn neo-btn--ghost" id="btnWarn">Warn</button>
    <button class="neo-btn neo-btn--ghost" id="btnOkDemo">Success</button>
    <button class="neo-btn neo-btn--ghost" id="btnDanger">Danger</button>
  </div>

  <div style="padding: 18px; max-width: 900px;">
    <h1 style="margin: 0 0 10px 0; font-weight: 720; letter-spacing: .2px;">UI Demo</h1>
    <p style="margin: 0; color: rgba(255,255,255,.70); line-height: 1.5;">
      Scorri e prova a trascinare l’alert. Chiusura: solo OK o ESC.
    </p>

    <div style="height: 120vh;"></div>
  </div>

  <!-- Alert: chiusura solo OK/ESC -->
  <div class="neo-alert" id="neoAlert"
       role="alertdialog"
       aria-modal="false"
       aria-live="polite"
       aria-labelledby="neoTitle"
       aria-describedby="neoMessage"
       data-state="closed">

    <div class="neo-head" id="neoHandle">
      <div class="neo-title" id="neoTitle">System Message</div>
      <div class="neo-badges">
        <span class="neo-chip neo-chip--info" id="neoType">INFO</span>
      </div>
    </div>

    <div class="neo-body">
      <div class="neo-message" id="neoMessage">...</div>
      <div class="neo-actions">
        <button class="neo-btn neo-btn--ghost" type="button" id="neoOk">OK</button>
      </div>
    </div>
  </div>

  <script src="script.js"></script>
</body>
</html>

2) CSS completo (styles.css)

Lo stile è “glassmorphism” con glow neon leggero. La posizione dell’alert viene controllata tramite variabili CSS --x e --y, aggiornate da JavaScript: è un modo pulito e fluido per gestire lo spostamento.

:root{
  --glass: rgba(255,255,255,.08);
  --glass2: rgba(255,255,255,.12);
  --stroke: rgba(255,255,255,.18);
  --text: rgba(255,255,255,.92);
  --muted: rgba(255,255,255,.68);

  --neonA: #7c4dff;
  --neonB: #00e5ff;

  --danger:#ff3d71;
  --warn:#ffcc00;
  --ok:#00e676;

  --shadow: 0 24px 80px rgba(0,0,0,.55);
}

.neo-btn{
  border: 1px solid rgba(255,255,255,.20);
  background: linear-gradient(90deg, rgba(124,77,255,.35), rgba(0,229,255,.25));
  color: var(--text);
  padding: 10px 12px;
  border-radius: 12px;
  cursor: pointer;
  transition: transform .12s ease, filter .12s ease;
}

.neo-btn:hover{ filter: brightness(1.08); }
.neo-btn:active{ transform: translateY(1px); }

.neo-btn--ghost{
  background: rgba(0,0,0,.16);
}

.neo-alert{
  position: fixed;
  top: 0; left: 0;
  width: min(420px, calc(100vw - 24px));
  border-radius: 16px;
  background: linear-gradient(180deg, var(--glass), var(--glass2));
  border: 1px solid var(--stroke);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  box-shadow: var(--shadow);
  color: var(--text);

  user-select: none;

  transform: translate3d(var(--x, -9999px), var(--y, -9999px), 0);

  opacity: 0;
  pointer-events: none;
  transition: opacity .18s ease;
}

.neo-alert::before{
  content:"";
  position:absolute;
  inset:-1px;
  border-radius: 16px;
  background: linear-gradient(90deg, var(--neonA), var(--neonB));
  filter: blur(10px);
  opacity: .28;
  z-index: -1;
}

.neo-alert[data-state="open"]{
  opacity: 1;
  pointer-events: auto;
}

.neo-head{
  display:flex;
  align-items:center;
  justify-content:space-between;
  padding: 12px 12px 10px 12px;
  cursor: grab;
}

.neo-head:active{ cursor: grabbing; }

.neo-title{
  font-weight: 650;
  letter-spacing: .3px;
}

.neo-badges{
  display:flex;
  align-items:center;
}

.neo-chip{
  font-size: 12px;
  padding: 4px 8px;
  border-radius: 999px;
  border: 1px solid rgba(255,255,255,.20);
  background: rgba(0,0,0,.18);
}

.neo-chip--info{ box-shadow: 0 0 0 1px rgba(0,229,255,.18) inset; }
.neo-chip--warn{ box-shadow: 0 0 0 1px rgba(255,204,0,.22) inset; }
.neo-chip--ok{ box-shadow: 0 0 0 1px rgba(0,230,118,.22) inset; }
.neo-chip--danger{ box-shadow: 0 0 0 1px rgba(255,61,113,.22) inset; }

.neo-body{ padding: 6px 12px 12px 12px; }
.neo-message{ color: var(--muted); line-height: 1.45; }
.neo-actions{ display:flex; justify-content:flex-end; margin-top: 12px; }

@media (prefers-reduced-motion: reduce){
  .neo-alert, .neo-btn{ transition: none; }
}

3) JavaScript completo (script.js)

Il drag usa Pointer Events (quindi funziona anche su mobile) e applica vincoli per impedire che l’alert esca dalla viewport. Quando l’alert si apre, viene centrato e il focus va su OK. La chiusura avviene con OK o con ESC.

class NeoAlert {
  constructor(opts) {
    this.box = opts.box;
    this.handle = opts.handle;
    this.titleEl = opts.titleEl;
    this.msgEl = opts.msgEl;
    this.typeEl = opts.typeEl;
    this.okBtn = opts.okBtn;

    this.state = {
      open: false,
      dragging: false,
      pointerId: null,
      offsetX: 0,
      offsetY: 0,
      x: 20,
      y: 20,
      lastFocus: null,
    };

    this.onPointerDown = this.onPointerDown.bind(this);
    this.onPointerMove = this.onPointerMove.bind(this);
    this.onPointerUp = this.onPointerUp.bind(this);
    this.onKeyDown = this.onKeyDown.bind(this);
    this.onResize = this.onResize.bind(this);

    this.handle.addEventListener("pointerdown", this.onPointerDown, { passive: false });
    this.okBtn.addEventListener("click", () => this.close());
    window.addEventListener("keydown", this.onKeyDown);
    window.addEventListener("resize", this.onResize);
  }

  show({ title, message, type = "info" }) {
    this.state.lastFocus = document.activeElement;

    this.titleEl.textContent = title ?? "System Message";
    this.msgEl.textContent = message ?? "";
    this.setType(type);

    this.box.dataset.state = "open";
    this.state.open = true;

    this.center();
    this.applyPosition();

    this.okBtn.focus({ preventScroll: true });
  }

  close() {
    if (!this.state.open) return;
    this.box.dataset.state = "closed";
    this.state.open = false;

    if (this.state.lastFocus && typeof this.state.lastFocus.focus === "function") {
      this.state.lastFocus.focus({ preventScroll: true });
    }
  }

  setType(type) {
    const map = {
      info:  { label: "INFO",   cls: "neo-chip--info" },
      warn:  { label: "WARN",   cls: "neo-chip--warn" },
      ok:    { label: "OK",     cls: "neo-chip--ok" },
      danger:{ label: "DANGER", cls: "neo-chip--danger" },
    };

    const t = map[type] ?? map.info;
    this.typeEl.textContent = t.label;
    this.typeEl.classList.remove("neo-chip--info","neo-chip--warn","neo-chip--ok","neo-chip--danger");
    this.typeEl.classList.add(t.cls);
  }

  center() {
    const rect = this.box.getBoundingClientRect();
    const x = Math.round((window.innerWidth - rect.width) / 2);
    const y = Math.round((window.innerHeight - rect.height) / 2);

    this.state.x = this.clamp(x, 8, window.innerWidth - rect.width - 8);
    this.state.y = this.clamp(y, 8, window.innerHeight - rect.height - 8);
  }

  clampToViewport() {
    const rect = this.box.getBoundingClientRect();
    this.state.x = this.clamp(this.state.x, 8, window.innerWidth - rect.width - 8);
    this.state.y = this.clamp(this.state.y, 8, window.innerHeight - rect.height - 8);
  }

  applyPosition() {
    this.clampToViewport();
    this.box.style.setProperty("--x", `${this.state.x}px`);
    this.box.style.setProperty("--y", `${this.state.y}px`);
  }

  onPointerDown(e) {
    if (!this.state.open) return;

    if (e.pointerType === "mouse" && e.button !== 0) return;

    e.preventDefault();

    this.state.dragging = true;
    this.state.pointerId = e.pointerId;

    const rect = this.box.getBoundingClientRect();
    this.state.offsetX = e.clientX - rect.left;
    this.state.offsetY = e.clientY - rect.top;

    this.handle.setPointerCapture(e.pointerId);
    this.handle.addEventListener("pointermove", this.onPointerMove, { passive: false });
    this.handle.addEventListener("pointerup", this.onPointerUp);
    this.handle.addEventListener("pointercancel", this.onPointerUp);
  }

  onPointerMove(e) {
    if (!this.state.dragging || e.pointerId !== this.state.pointerId) return;
    e.preventDefault();

    const rect = this.box.getBoundingClientRect();
    const w = rect.width;
    const h = rect.height;

    const newX = e.clientX - this.state.offsetX;
    const newY = e.clientY - this.state.offsetY;

    this.state.x = this.clamp(newX, 8, window.innerWidth - w - 8);
    this.state.y = this.clamp(newY, 8, window.innerHeight - h - 8);

    this.applyPosition();
  }

  onPointerUp(e) {
    if (e.pointerId !== this.state.pointerId) return;

    this.state.dragging = false;
    this.state.pointerId = null;

    this.handle.removeEventListener("pointermove", this.onPointerMove);
    this.handle.removeEventListener("pointerup", this.onPointerUp);
    this.handle.removeEventListener("pointercancel", this.onPointerUp);
  }

  onKeyDown(e) {
    if (!this.state.open) return;
    if (e.key === "Escape") this.close();
  }

  onResize() {
    if (!this.state.open) return;
    this.applyPosition();
  }

  clamp(v, min, max) {
    return Math.max(min, Math.min(v, max));
  }
}

// Boot
const neo = new NeoAlert({
  box: document.getElementById("neoAlert"),
  handle: document.getElementById("neoHandle"),
  titleEl: document.getElementById("neoTitle"),
  msgEl: document.getElementById("neoMessage"),
  typeEl: document.getElementById("neoType"),
  okBtn: document.getElementById("neoOk"),
});

// Demo buttons
document.getElementById("btnDemo").addEventListener("click", () => {
  neo.show({
    title: "System Message",
    message: "Operazione completata. Chiusura: OK o ESC. Drag attivo (mouse/touch).",
    type: "info",
  });
});

document.getElementById("btnWarn").addEventListener("click", () => {
  neo.show({
    title: "Warning",
    message: "Parametro fuori soglia: verifica input e riprova.",
    type: "warn",
  });
});

document.getElementById("btnOkDemo").addEventListener("click", () => {
  neo.show({
    title: "Success",
    message: "Commit registrato. Stato coerente.",
    type: "ok",
  });
});

document.getElementById("btnDanger").addEventListener("click", () => {
  neo.show({
    title: "Danger",
    message: "Errore critico: operazione annullata. Nessuna modifica persistita.",
    type: "danger",
  });
});

 

Trascina l’alert dalla barra superiore. Chiusura: OK oppure ESC.
System Message
INFO
...
Messaggio sponsorizzato
Pubblicità

🔗 Condividi l'articolo:

Autore: Redazione · Creato: 04/03/2026 22:05 · Ultima modifica: 04/03/2026 22:59