Se rendre au contenu

Welcome .


Sign u

Cette question a été signalée

Formulaire de contact en verre dépoli avec effets “liquide” (blobs animés), typographie soignée et focus rings façon iOS/macOS. 100% vanilla (aucune dépendance), responsive, accessible (labels, aria-live, contraste), anti-spam (honeypot) et respect de prefers-reduced-motion.

Pour l’envoi d’e-mail sans backend, remplacez l’ID Formspree (YOUR_FORMSPREE_ID) par le vôtre ; sinon, branchez votre propre endpoint d’API.

<!doctype html>
<html lang="fr">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Formulaire — Liquid Glass (style Apple)</title>
  <!--
  Plug & Play — Formulaire "Liquid Glass" (style Apple)
  -----------------------------------------------------
  • Déposez ce fichier tel quel dans votre site (ou copiez le <section id="contact">).
  • Pour l'envoi d'email sans backend, remplacez YOUR_FORMSPREE_ID ci‑dessous.
  • Accessibilité, validation, anti‑spam (honeypot), respect de prefers-reduced-motion.
  -->
  <style>
    :root {
      --glass-bg: rgba(255, 255, 255, .28);
      --glass-brd: rgba(255, 255, 255, .45);
      --glass-blur: 22px;
      --text: #0b1220;
      --muted: #5b6170;
      --ring: #3b82f6; /* bleu doux façon iOS */
      --ok: #0ea5e9;
      --error: #ef4444;
    }

    * { box-sizing: border-box; }
    html, body { height: 100%; }
    body {
      margin: 0; color: var(--text);
      font-family: ui-sans-serif, system-ui, -apple-system, "SF Pro Text", Roboto, Arial, sans-serif;
      /* Fond dégradé avec halo, proche des visuels Apple */
      background: radial-gradient(1200px 800px at 20% -10%, #a5b4fc55, transparent 60%),
                  radial-gradient(1000px 700px at 120% 40%, #67e8f955, transparent 60%),
                  linear-gradient(135deg, #0f172a 0%, #111827 100%);
    }

    .wrap { min-height: 100%; display: grid; place-items: center; padding: clamp(16px, 3vw, 32px); }

    /* Blobs liquides en arrière-plan */
    .blobs { position: fixed; inset: -10% -10% auto -10%; pointer-events: none; z-index: 0; filter: blur(40px); opacity: .9; }
    .blob { position: absolute; width: 42vmin; height: 42vmin; border-radius: 40% 60% 55% 45% / 55% 45% 55% 45%; mix-blend-mode: screen; }
    .blob:nth-child(1){ background: #60a5fa; top: 8%; left: 8%; animation: float 16s ease-in-out infinite; }
    .blob:nth-child(2){ background: #a78bfa; top: 20%; right: 12%; animation: float 20s ease-in-out infinite reverse; }
    .blob:nth-child(3){ background: #34d399; bottom: 8%; left: 18%; animation: float 24s ease-in-out infinite; }
    @keyframes float { 0%,100%{ transform: translate3d(0,0,0) scale(1); } 50%{ transform: translate3d(0,-3%,0) scale(1.06); } }

    /* Carte verre dépoli */
    .card {
      position: relative; z-index: 1; width: min(720px, 92vw);
      background: var(--glass-bg);
      border: 1px solid var(--glass-brd);
      border-radius: 28px;
      box-shadow: 0 10px 40px rgba(0,0,0,.25), inset 0 1px 0 rgba(255,255,255,.25);
      backdrop-filter: saturate(160%) blur(var(--glass-blur));
      -webkit-backdrop-filter: saturate(160%) blur(var(--glass-blur));
      overflow: hidden;
    }

    .header { padding: clamp(18px, 3vw, 28px); border-bottom: 1px solid rgba(255,255,255,.35); }
    h1 { margin: 0; font-size: clamp(1.25rem, 2.2vw, 1.6rem); color: #ffffff; letter-spacing: .2px; }
    p.sub { margin: 6px 0 0; color: #e5e7eb; opacity: .9; font-size: .95rem; }

    form { display: grid; gap: 16px; padding: clamp(18px, 3vw, 28px); }
    .row { display: grid; gap: 14px; grid-template-columns: 1fr 1fr; }

    label { color: #e5e7eb; font-weight: 600; font-size: .95rem; }

    .field { display: grid; gap: 8px; }
    input, textarea {
      appearance: none; width: 100%; color: #e5e7eb;
      background: rgba(255,255,255,.08);
      border: 1px solid rgba(255,255,255,.28);
      border-radius: 16px; padding: 14px 16px;
      outline: none; transition: box-shadow .2s ease, border-color .2s ease, background .2s ease;
      backdrop-filter: blur(12px);
    }
    input::placeholder, textarea::placeholder { color: #cbd5e1; opacity: .7; }
    input:focus, textarea:focus {
      border-color: rgba(59,130,246,.75);
      box-shadow: 0 0 0 4px rgba(59,130,246,.25), inset 0 1px 0 rgba(255,255,255,.2);
      background: rgba(255,255,255,.12);
    }

    .hint { color: #cbd5e1; opacity: .9; font-size: .9rem; }

    .actions { display: flex; gap: 12px; align-items: center; margin-top: 6px; }
    button {
      appearance: none; border: 1px solid rgba(255,255,255,.35);
      background: linear-gradient(180deg, #ffffffcc, #dbeafeaa);
      color: #0b1220; font-weight: 700; letter-spacing: .2px;
      padding: 12px 18px; border-radius: 14px; cursor: pointer;
      box-shadow: inset 0 1px 0 rgba(255,255,255,.8), 0 10px 16px rgba(59,130,246,.25);
      transition: transform .06s ease, box-shadow .2s ease, filter .2s ease;
    }
    button:hover { transform: translateY(-1px); }
    button:active { transform: translateY(0); box-shadow: inset 0 1px 0 rgba(255,255,255,.8), 0 6px 12px rgba(59,130,246,.25); }
    button[disabled] { opacity: .7; cursor: not-allowed; filter: grayscale(.2); }

    .status { font-size: .95rem; color: #e5e7eb; }
    .ok { color: #86efac; }
    .err { color: #fecaca; }

    /* Badge discret en haut‑droite */
    .badge {
      position: absolute; top: 10px; right: 12px; font-size: .8rem; color: #e5e7eb;
      background: rgba(255,255,255,.08); border: 1px solid rgba(255,255,255,.25);
      padding: 6px 10px; border-radius: 999px; backdrop-filter: blur(8px);
    }

    /* Responsive */
    @media (max-width: 680px) { .row { grid-template-columns: 1fr; } }

    /* Réduction de mouvement */
    @media (prefers-reduced-motion: reduce) {
      .blob { animation: none; }
      button, input, textarea { transition: none; }
    }
  </style>
</head>
<body>
  <div class="wrap">
    <!-- Blobs liquides -->
    <div class="blobs" aria-hidden="true">
      <div class="blob"></div>
      <div class="blob"></div>
      <div class="blob"></div>
    </div>

    <!-- Carte verre dépoli -->
    <section class="card" role="region" aria-labelledby="title" id="contact">
      <div class="badge" aria-hidden="true">Liquid Glass</div>
      <div class="header">
        <h1 id="title">Nous contacter</h1>
        <p class="sub">Un formulaire élégant en verre dépoli, façon iOS/macOS.</p>
      </div>

      <form id="contact-form" novalidate>
        <div class="row">
          <div class="field">
            <label for="name">Nom</label>
            <input id="name" name="name" type="text" placeholder="Votre nom" autocomplete="name" required />
          </div>
          <div class="field">
            <label for="email">Email</label>
            <input id="email" name="email" type="email" placeholder="vous@domaine.com" autocomplete="email" required />
          </div>
        </div>

        <div class="field">
          <label for="subject">Sujet</label>
          <input id="subject" name="subject" type="text" placeholder="Sujet du message" required />
        </div>

        <div class="field">
          <label for="message">Message</label>
          <textarea id="message" name="message" rows="6" placeholder="Votre message…" required></textarea>
          <div class="hint">En envoyant, vous acceptez d'être recontacté concernant votre demande.</div>
        </div>

        <!-- Honeypot anti‑spam -->
        <div style="position:absolute; left:-5000px" aria-hidden="true">
          <label for="website">Ne pas remplir</label>
          <input id="website" name="website" type="text" tabindex="-1" autocomplete="off" />
        </div>

        <div class="actions">
          <button type="submit" id="sendBtn">Envoyer</button>
          <span class="status" id="status" aria-live="polite"></span>
        </div>
      </form>
    </section>
  </div>

  <script>
    // Remplacez par votre endpoint (Formspree, votre API, etc.)
    const FORMSPREE_ENDPOINT = "https://formspree.io/f/YOUR_FORMSPREE_ID";

    const form = document.getElementById('contact-form');
    const statusEl = document.getElementById('status');
    const sendBtn = document.getElementById('sendBtn');

    function validEmail(v){ return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); }

    form.addEventListener('submit', async (e) => {
      e.preventDefault();
      statusEl.textContent = '';
      const data = new FormData(form);

      // honeypot
      if ((data.get('website')||'').trim() !== '') {
        statusEl.textContent = 'Envoi bloqué.';
        statusEl.className = 'status err';
        return;
      }

      const name = (data.get('name')||'').trim();
      const email = (data.get('email')||'').trim();
      const subject = (data.get('subject')||'').trim();
      const message = (data.get('message')||'').trim();

      if (!name || !email || !subject || !message) {
        statusEl.textContent = 'Veuillez remplir tous les champs requis.';
        statusEl.className = 'status err';
        return;
      }
      if (!validEmail(email)) {
        statusEl.textContent = 'Adresse email invalide.';
        statusEl.className = 'status err';
        return;
      }

      sendBtn.disabled = true; sendBtn.textContent = 'Envoi…';
      try {
        data.append('_replyto', email);
        data.append('_subject', subject || 'Nouveau message via formulaire');

        const res = await fetch(FORMSPREE_ENDPOINT, { method: 'POST', body: data, headers: { 'Accept': 'application/json' } });
        if (res.ok) {
          form.reset();
          statusEl.textContent = 'Message envoyé. Merci !';
          statusEl.className = 'status ok';
        } else {
          const info = await res.json().catch(() => ({}));
          const msg = (info && (info.error || info.message)) || 'Une erreur est survenue.';
          statusEl.textContent = msg; statusEl.className = 'status err';
        }
      } catch (err) {
        statusEl.textContent = 'Réseau indisponible. Réessayez plus tard.';
        statusEl.className = 'status err';
      } finally {
        sendBtn.disabled = false; sendBtn.textContent = 'Envoyer';
      }
    });
  </script>
</body>
</html>

Ignorer
Publications associées Réponses Vues Activité
0
août 25
3
0
août 25
4
0
août 25
57
0
août 25
102
0
août 25
56