SnippetUI

Menu Popover

Menu popover yang terbuka saat diklik (atau fokus) dan menampilkan daftar tindakan, bukan saat diarahkan.

HTMLCSSJavaScript

Menu popover, tidak seperti tooltip yang muncul saat mengarahkan kursor, terbuka saat diklik dan digunakan untuk menyajikan daftar tindakan (seperti edit atau hapus) kepada pengguna. Ini dirancang dengan mempertimbangkan aksesibilitas, seperti manajemen fokus dan pemrosesan penutupan non-klik.

Pratinjau

Kasus & Fitur Penggunaan

  • Tampilkan opsi: Ideal untuk UI yang menampilkan berbagai tindakan (edit, hapus, bagikan, dll.) dengan mengklik ikon menu (…) atau tombol.

  • Pertimbangan aksesibilitas: Menggunakan atribut aria-expanded dan aria-haspopup untuk mendukung penyembunyian dan manajemen fokus dengan tombol Escape.

  • Animasi halus: Untuk mencegah peralihan tampilan yang tidak wajar, kami telah menerapkan transisi yang menggabungkan geser ke arah sumbu Y dan fade-in.

Poin-poin penting dari desain dan UX (Desain & UX)

  • Hapus Hierarki: Bayangan (box-shadow) digunakan untuk menciptakan perasaan mengambang dan memisahkan konten dengan jelas dari konten latar belakang.

  • Pengoperasian aman: Termasuk proses menutup menu dengan menekan area di luar rentang klik (Klik Luar), membuatnya intuitif untuk dioperasikan pada ponsel dan PC.

  • Menyoroti tindakan merusak: Kami menggunakan teks merah (Bahaya) untuk tindakan yang tidak dapat dibatalkan, seperti penghapusan, untuk mengurangi kesalahan pengguna.

Cara menyesuaikan (Panduan Kustomisasi)

  • Jika Anda ingin mengubah lebar menu, silakan sesuaikan width dari .popover-menu.

  • Jika Anda ingin arah pembukaan menjadi kanan bawah atau kiri bawah, Anda dapat dengan mudah menyesuaikannya dengan mengubah properti transform dan left dari .popover-menu.

Kode implementasi (Implementasi)

Anda dapat memasukkan menu popover dalam proyek Anda menggunakan kode di bawah ini.

<div class="popover-wrapper">

<button class="popover-trigger" aria-haspopup="true" aria-expanded="false" id="popover-btn">

  <span>tindakan</span>

  <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">

    <path d="m6 9 6 6 6-6"/>

  </svg>

</button>

<div class="popover-menu" role="menu" aria-labelledby="popover-btn">

  <div class="popover-content">

    <button class="popover-item" role="menuitem">

      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>

      Sunting

    </button>

    <button class="popover-item" role="menuitem">

      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect width="18" height="18" x="3" y="3" rx="2" ry="2"/><path d="M21 9H3"/><path d="M9 21V9"/></svg>

      duplikat

    </button>

    <div class="popover-divider"></div>

    <button class="popover-item text-danger" role="menuitem">

      <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>

      hapus

    </button>

  </div>

</div>

</div>



<style>
/* Tempatkan CSS di sini */

.popover-wrapper {

position: relative;

display: inline-block;

}



.popover-trigger {

display: flex;

align-items: center;

gap: 6px;

padding: 8px 16px;

font-size: 14px;

font-weight: 500;

color: #334155;

background-color: #ffffff;

border: 1px solid #cbd5e1;

border-radius: 8px;

cursor: pointer;

transition: all 0.2s ease;

box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);

}



.popover-trigger:hover {

background-color: #f1f5f9;

border-color: #94a3b8;

}



.popover-trigger:focus-visible {

outline: 2px solid #3b82f6;

outline-offset: 2px;

}



.popover-trigger svg {

transition: transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);

}



.popover-trigger.is-open svg {

transform: rotate(180deg);

}



.popover-menu {

position: absolute;

top: calc(100% + 8px);

left: 50%;

transform: translateX(-50%) translateY(-10px);

width: 200px;

background: #ffffff;

border-radius: 12px;

box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgba(0, 0, 0, 0.1);

border: 1px solid #e2e8f0;

opacity: 0;

visibility: hidden;

transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);

z-index: 50;

pointer-events: none;

}



.popover-wrapper.is-active .popover-menu {

opacity: 1;

visibility: visible;

transform: translateX(-50%) translateY(0);

pointer-events: auto;

}



.popover-content {

padding: 6px;

display: flex;

flex-direction: column;

gap: 2px;

}



.popover-item {

display: flex;

align-items: center;

gap: 10px;

width: 100%;

padding: 8px 12px;

font-size: 14px;

color: #475569;

background: transparent;

border: none;

border-radius: 6px;

cursor: pointer;

text-align: left;

transition: background-color 0.15s ease, color 0.15s ease;

}



.popover-item:hover,

.popover-item:focus-visible {

background-color: #f1f5f9;

color: #0f172a;

outline: none;

}



.popover-item.text-danger {

color: #ef4444;

}



.popover-item.text-danger:hover,

.popover-item.text-danger:focus-visible {

background-color: #fef2f2;

color: #dc2626;

}



.popover-divider {

height: 1px;

background-color: #e2e8f0;

margin: 4px 0;

}
</style>



<script>

// Tempatkan JavaScript di sini

document.addEventListener('DOMContentLoaded', () => {

const wrappers = document.querySelectorAll('.popover-wrapper');



wrappers.forEach(wrapper => {

  const trigger = wrapper.querySelector('.popover-trigger');

  const menu = wrapper.querySelector('.popover-menu');

  

  if (!trigger || !menu) return;



  const togglePopover = () => {

    const isActive = wrapper.classList.contains('is-active');

    if (isActive) {

      closePopover();

    } else {

      openPopover();

    }

  };



  const openPopover = () => {

    wrapper.classList.add('is-active');

    trigger.classList.add('is-open');

    trigger.setAttribute('aria-expanded', 'true');

  };



  const closePopover = () => {

    wrapper.classList.remove('is-active');

    trigger.classList.remove('is-open');

    trigger.setAttribute('aria-expanded', 'false');

  };



  trigger.addEventListener('click', togglePopover);



  //Tutup di luar area klik

  document.addEventListener('click', (e) => {

    if (!wrapper.contains(e.target)) {

      closePopover();

    }

  });



  // Tutup dengan tombol Escape

  document.addEventListener('keydown', (e) => {

    if (e.key === 'Escape' && wrapper.classList.contains('is-active')) {

      closePopover();

      trigger.focus();

    }

  });

});

});

</script>