Umpan Balik & Dialog
Toast (Notifikasi Mengambang)
Pesan umpan balik sementara yang muncul di sudut layar untuk mengabarkan hasil eksekusi tindakan.
Toast memberikan konfirmasi cepat (misal: "Berkas berhasil disimpan sebagai draf") tanpa memblokir interaksi pengguna lain di layar.
Pratinjau Komponen
Permohonan Berhasil Dikirim
Nomor resi bukti pengajuan adalah #MJL-2026-9812. Cek email Anda untuk rincian.
Penggunaan CSS Murni
<div class="mjl-toast-container">
<div class="mjl-toast" role="status">
<div class="mjl-toast__content">
<div class="mjl-toast__title">Berhasil</div>
<div class="mjl-toast__desc">Data berhasil diperbarui.</div>
</div>
<button class="mjl-toast__close" aria-label="Tutup">✕</button>
</div>
</div>
Penggunaan di Berbagai Teknologi
Salin contoh sesuai teknologi proyek Anda. Semua versi memakai class dari @majalengka/css. Komponen siap pakai tersedia di @majalengka/vue; untuk React, Angular, Laravel, dan PHP, contoh di bawah sudah berupa pembungkus kecil yang bisa langsung disalin ke proyek.
<!-- Wadah live region: render SEKALI saat halaman dimuat, sebelum toast pertama muncul -->
<div class="mjl-toast-container" role="region" aria-label="Notifikasi" aria-live="polite">
<div class="mjl-toast">
<div class="mjl-toast__content">
<div class="mjl-toast__title">Data tersimpan</div>
<div class="mjl-toast__desc">Perubahan profil berhasil disimpan.</div>
</div>
<button type="button" class="mjl-toast__close" aria-label="Tutup notifikasi">✕</button>
</div>
</div>
<script setup lang="ts">
import { ref } from 'vue';
import { MjlToast, MjlToastContainer, MjlButton } from '@majalengka/vue';
const toasts = ref<{ id: number; title: string; desc: string }[]>([]);
let id = 0;
const tampilkan = () => toasts.value.push({ id: ++id, title: 'Data tersimpan', desc: 'Perubahan profil berhasil disimpan.' });
const hapus = (tid: number) => (toasts.value = toasts.value.filter((t) => t.id !== tid));
</script>
<template>
<MjlButton @click="tampilkan">Simpan</MjlButton>
<MjlToastContainer>
<MjlToast v-for="t in toasts" :key="t.id" :title="t.title" :description="t.desc" @close="hapus(t.id)" />
</MjlToastContainer>
</template>
import { useState } from 'react';
type Toast = { id: number; title: string; desc?: string };
export function useToasts() {
const [toasts, setToasts] = useState<Toast[]>([]);
const show = (t: Omit<Toast, 'id'>) => setToasts((all) => [...all, { ...t, id: Date.now() }]);
const close = (id: number) => setToasts((all) => all.filter((t) => t.id !== id));
// Render <ToastContainer /> sekali di layout agar region live sudah ada sebelum toast muncul.
const ToastContainer = () => (
<div className="mjl-toast-container" role="region" aria-label="Notifikasi" aria-live="polite">
{toasts.map((t) => (
<div key={t.id} className="mjl-toast">
<div className="mjl-toast__content">
<div className="mjl-toast__title">{t.title}</div>
{t.desc && <div className="mjl-toast__desc">{t.desc}</div>}
</div>
<button type="button" className="mjl-toast__close" aria-label="Tutup notifikasi" onClick={() => close(t.id)}>✕</button>
</div>
))}
</div>
);
return { show, ToastContainer };
}
import { Component, Injectable, inject, signal } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ToastService {
toasts = signal<{ id: number; title: string; desc?: string }[]>([]);
show(title: string, desc?: string) { this.toasts.update((t) => [...t, { id: Date.now(), title, desc }]); }
close(id: number) { this.toasts.update((t) => t.filter((x) => x.id !== id)); }
}
// Taruh <mjl-toast-container /> sekali di AppComponent.
@Component({
selector: 'mjl-toast-container',
standalone: true,
template: `
<div class="mjl-toast-container" role="region" aria-label="Notifikasi" aria-live="polite">
@for (t of toast.toasts(); track t.id) {
<div class="mjl-toast">
<div class="mjl-toast__content">
<div class="mjl-toast__title">{{ t.title }}</div>
@if (t.desc) { <div class="mjl-toast__desc">{{ t.desc }}</div> }
</div>
<button type="button" class="mjl-toast__close" aria-label="Tutup notifikasi" (click)="toast.close(t.id)">✕</button>
</div>
}
</div>
`,
})
export class MjlToastContainer {
toast = inject(ToastService);
}
{{-- resources/views/layouts/app.blade.php — tampilkan session flash sebagai toast --}}
<div class="mjl-toast-container" role="region" aria-label="Notifikasi" aria-live="polite">
@if (session('toast'))
<div class="mjl-toast">
<div class="mjl-toast__content">
<div class="mjl-toast__title">{{ session('toast') }}</div>
</div>
<button type="button" class="mjl-toast__close" aria-label="Tutup notifikasi" onclick="this.closest('.mjl-toast').remove()">✕</button>
</div>
@endif
</div>
{{-- Di controller: return redirect()->route('profil')->with('toast', 'Data tersimpan'); --}}
<?php session_start(); ?>
<div class="mjl-toast-container" role="region" aria-label="Notifikasi" aria-live="polite">
<?php if (!empty($_SESSION['toast'])): ?>
<div class="mjl-toast">
<div class="mjl-toast__content">
<div class="mjl-toast__title"><?= htmlspecialchars($_SESSION['toast'], ENT_QUOTES, 'UTF-8') ?></div>
</div>
<button type="button" class="mjl-toast__close" aria-label="Tutup notifikasi" onclick="this.closest('.mjl-toast').remove()">✕</button>
</div>
<?php unset($_SESSION['toast']); ?>
<?php endif; ?>
</div>
Default duration adalah 0 (tidak hilang otomatis) demi WCAG 2.2.1. Bila diisi, hitungan berhenti saat toast di-hover atau difokus.