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",
});
});