Tips & Tricks (Diperbarui: 2/6/2026)

Membuat Toast Notification React yang Aksesibel dengan Claude Code

Panduan React toast dengan antrean, auto-dismiss, pause, aria-live, reduced motion, dan mobile safe area.

Membuat Toast Notification React yang Aksesibel dengan Claude Code

Toast notification terlihat kecil: “Tersimpan”, “Ekspor dimulai”, “Pembayaran gagal”. Di produk nyata, komponen kecil ini memengaruhi aksesibilitas, rasa percaya, tampilan mobile, dan konversi. Toast yang buruk hilang terlalu cepat, memakai alert untuk semua pesan, menutupi CTA utama, atau menjadi satu-satunya tempat munculnya error penting.

Claude Code bisa membuat tampilan toast dengan cepat. Agar layak produksi, prompt harus meminta antrean, auto-dismiss, pause saat hover dan focus, pilihan role="status" versus role="alert", prefers-reduced-motion, safe area mobile, dan review yang kritis. Bacaan terkait: Claude Code accessibility, animation implementation, responsive design, dan React development.

Aturan desain

Toast bukan modal. Ia tidak boleh mengambil focus atau menghentikan pekerjaan user. Gunakan untuk feedback pendek yang tidak memblokir. Jika user harus memperbaiki field, mengonfirmasi aksi berisiko, memulihkan pembayaran, atau membaca instruksi panjang, tampilkan juga pesan yang menetap di halaman.

Implementasi ini memakai aturan berikut:

  • maksimal 3 toast terlihat
  • success, info, dan warning memakai role="status"
  • error memakai role="alert" hanya untuk masalah mendesak
  • auto-dismiss, tetapi pause saat hover dan focus
  • setiap toast punya tombol tutup
  • animasi mati saat prefers-reduced-motion
  • spacing mobile memakai env(safe-area-inset-*)

Rujukan resmi: MDN status role, MDN alert role, W3C WCAG Status Messages, W3C WCAG Pause, Stop, Hide, MDN setTimeout, MDN prefers-reduced-motion, dan Claude Code docs.

Kode React siap salin

Buat ToastProvider.tsx. File ini hanya butuh React. Untuk Next.js App Router, tambahkan "use client"; di baris pertama.

import {
  createContext,
  useCallback,
  useContext,
  useEffect,
  useMemo,
  useRef,
  useState,
  type ReactNode,
} from "react";

type ToastTone = "success" | "info" | "warning" | "error";
type ToastInput = { title: string; description?: string; tone?: ToastTone; durationMs?: number };
type ToastItem = Required<Omit<ToastInput, "durationMs">> & { id: string; durationMs: number; createdAt: number };
type ToastContextValue = { showToast: (input: ToastInput) => string; dismissToast: (id: string) => void };

const ToastContext = createContext<ToastContextValue | null>(null);
const MAX_VISIBLE_TOASTS = 3;
const DEFAULT_DURATION = 5000;

export function ToastProvider({ children }: { children: ReactNode }) {
  const [toasts, setToasts] = useState<ToastItem[]>([]);
  const dismissToast = useCallback((id: string) => {
    setToasts((current) => current.filter((toast) => toast.id !== id));
  }, []);
  const showToast = useCallback((input: ToastInput) => {
    const id = crypto.randomUUID();
    const nextToast: ToastItem = {
      id,
      title: input.title,
      description: input.description ?? "",
      tone: input.tone ?? "info",
      durationMs: input.durationMs ?? DEFAULT_DURATION,
      createdAt: Date.now(),
    };
    setToasts((current) => [...current, nextToast].slice(-MAX_VISIBLE_TOASTS));
    return id;
  }, []);
  const value = useMemo(() => ({ showToast, dismissToast }), [showToast, dismissToast]);
  return (
    <ToastContext.Provider value={value}>
      {children}
      <ToastViewport toasts={toasts} onDismiss={dismissToast} />
    </ToastContext.Provider>
  );
}

export function useToast() {
  const context = useContext(ToastContext);
  if (!context) throw new Error("useToast must be used inside ToastProvider");
  return context;
}

function ToastViewport({ toasts, onDismiss }: { toasts: ToastItem[]; onDismiss: (id: string) => void }) {
  return (
    <div className="toast-viewport" aria-label="Notifikasi">
      {toasts.map((toast) => (
        <ToastCard key={toast.id} toast={toast} onDismiss={onDismiss} />
      ))}
    </div>
  );
}

function ToastCard({ toast, onDismiss }: { toast: ToastItem; onDismiss: (id: string) => void }) {
  const [paused, setPaused] = useState(false);
  const remainingMs = useRef(toast.durationMs);
  const startedAt = useRef<number | null>(null);
  const timeoutId = useRef<number | null>(null);

  useEffect(() => {
    if (toast.durationMs <= 0 || paused) return;
    startedAt.current = Date.now();
    timeoutId.current = window.setTimeout(() => onDismiss(toast.id), remainingMs.current);
    return () => {
      if (timeoutId.current !== null) window.clearTimeout(timeoutId.current);
      if (startedAt.current !== null) remainingMs.current -= Date.now() - startedAt.current;
    };
  }, [onDismiss, paused, toast.durationMs, toast.id]);

  const role = toast.tone === "error" ? "alert" : "status";
  return (
    <section
      className={`toast-card toast-card--${toast.tone}`}
      role={role}
      aria-atomic="true"
      onMouseEnter={() => setPaused(true)}
      onMouseLeave={() => setPaused(false)}
      onFocus={() => setPaused(true)}
      onBlur={() => setPaused(false)}
    >
      <div className="toast-card__content">
        <strong className="toast-card__title">{toast.title}</strong>
        {toast.description ? <p>{toast.description}</p> : null}
      </div>
      <button type="button" className="toast-card__close" aria-label={`Tutup ${toast.title}`} onClick={() => onDismiss(toast.id)}>
        ×
      </button>
    </section>
  );
}

Tambahkan toast.css.

.toast-viewport {
  position: fixed;
  top: max(16px, env(safe-area-inset-top));
  right: max(16px, env(safe-area-inset-right));
  z-index: 1000;
  display: grid;
  gap: 10px;
  width: min(380px, calc(100vw - 32px));
  pointer-events: none;
}
.toast-card {
  pointer-events: auto;
  display: grid;
  grid-template-columns: 1fr auto;
  align-items: start;
  gap: 12px;
  padding: 14px 14px 14px 16px;
  border: 1px solid #d8dee8;
  border-left-width: 5px;
  border-radius: 8px;
  background: #fff;
  color: #172033;
  box-shadow: 0 12px 30px rgba(15, 23, 42, 0.18);
  animation: toast-slide-in 180ms ease-out;
}
.toast-card--success { border-left-color: #15803d; }
.toast-card--info { border-left-color: #2563eb; }
.toast-card--warning { border-left-color: #b45309; }
.toast-card--error { border-left-color: #b91c1c; }
.toast-card__title { display: block; font-size: 0.95rem; line-height: 1.35; }
.toast-card p { margin: 4px 0 0; color: #46536a; font-size: 0.875rem; line-height: 1.5; }
.toast-card__close {
  min-width: 32px;
  min-height: 32px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: #526071;
  cursor: pointer;
  font-size: 1.25rem;
  line-height: 1;
}
.toast-card__close:hover,
.toast-card__close:focus-visible { background: #eef2f7; outline: 2px solid transparent; }
@keyframes toast-slide-in {
  from { opacity: 0; transform: translateY(-8px); }
  to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 640px) {
  .toast-viewport { left: 16px; right: 16px; width: auto; }
}
@media (prefers-reduced-motion: reduce) {
  .toast-card { animation: none; }
}

Contoh penggunaan:

import { ToastProvider, useToast } from "./ToastProvider";
import "./toast.css";

function SaveProfileButton() {
  const { showToast } = useToast();

  async function handleSave() {
    try {
      await new Promise((resolve) => window.setTimeout(resolve, 600));
      showToast({
        tone: "success",
        title: "Profil tersimpan",
        description: "Perubahan akan muncul saat layar ini dibuka lagi.",
      });
    } catch {
      showToast({
        tone: "error",
        title: "Gagal menyimpan",
        description: "Periksa koneksi lalu coba lagi.",
        durationMs: 8000,
      });
    }
  }

  return <button onClick={handleSave}>Simpan</button>;
}

export default function App() {
  return (
    <ToastProvider>
      <main>
        <h1>Pengaturan</h1>
        <SaveProfileButton />
      </main>
    </ToastProvider>
  );
}

3 use case nyata

Pertama, halaman pengaturan. Toast sukses membuat user yakin data sudah tersimpan tanpa modal. Error validasi tetap harus berada dekat field; toast hanya merangkum.

Kedua, pekerjaan background seperti ekspor CSV, ringkasan AI, pemrosesan gambar, atau pengiriman email. Minta Claude Code membuat state mulai, sukses, gagal, batal, dan retry.

Ketiga, funnel monetisasi. Toast dapat mengonfirmasi PDF gratis sudah dikirim, permintaan konsultasi diterima, atau download berbayar disiapkan. Jangan biarkan toast menutupi harga, link Gumroad, form newsletter, atau tombol sticky mobile. Untuk pengukuran, sambungkan dengan Claude Code analytics implementation.

Kesalahan umum

Jangan memakai role="alert" untuk semua pesan. Success dan info biasa cukup memakai status; alert hanya untuk error mendesak.

Jangan auto-dismiss terlalu cepat. Lima detik adalah default yang aman, dan error sebaiknya lebih lama. Pause saat hover/focus memberi waktu untuk membaca dan menutup.

Jangan taruh informasi kritis hanya di toast. Payment failure, masalah permission, dan form validation harus tetap terlihat di halaman.

Hindari blinking dan animasi berulang. Animasi masuk harus pendek dan mati saat reduced motion.

Prompt untuk Claude Code

Implementasikan toast notification yang aksesibel dengan React + TypeScript.
Edit hanya ToastProvider.tsx dan toast.css.

Kebutuhan:
- tone success/info/warning/error
- maksimum 3 toast terlihat
- auto-dismiss, tombol tutup, pause saat hover/focus
- role="status" untuk pesan tidak mendesak
- role="alert" hanya untuk error mendesak
- aria-atomic="true"
- dukung prefers-reduced-motion
- dukung mobile safe-area
- error form penting tidak boleh hanya di toast

Verifikasi:
- npm run typecheck
- npm run lint
- test sukses, error, lebih dari 3 toast, hover pause, focus pause, keyboard close
Review implementasi toast ini secara kritis.
Cek status/alert, waktu auto-dismiss, pause hover/focus,
reduced motion, mobile safe areas, akses keyboard, dan informasi penting yang hilang.
Kembalikan temuan berdasarkan severity dengan file, baris, dan solusi konkret.

Masukkan checklist ini ke CLAUDE.md best practices dan review workflow checklist.

Verifikasi langsung

Saya mencoba pola ini di proyek React kecil dengan ToastProvider.tsx dan toast.css terpisah. Saya memicu success, error, lebih dari tiga toast, hover pause, focus pause, tombol tutup, dan reduced motion. Sisa waktu disimpan di useRef, jadi setelah pause timer tidak mulai lagi dari lima detik. Di production, tambahkan Playwright, axe, dan pengecekan manual dengan screen reader.

Penutup

Toast kecil, tetapi berdampak pada aksesibilitas, kepercayaan, dan konversi mobile. Saat meminta Claude Code, jelaskan perilaku, batasan, dan kriteria review, bukan hanya bentuk visual.

Untuk belajar sendiri, mulai dari free Claude Code cheatsheet. Untuk prompt dan materi siap pakai, lihat produk ClaudeCodeLab. Untuk tim yang ingin menstandardisasi UI review, aksesibilitas, dan guardrail monetisasi, mulai dari Claude Code training and consultation.

#Claude Code #toast #notification #React #UI
Gratis

PDF gratis: cheatsheet Claude Code

Masukkan email dan unduh satu halaman berisi command, kebiasaan review, dan workflow aman.

Kami menjaga datamu dan tidak mengirim spam.

Masa

Tentang penulis

Masa

Engineer yang berfokus pada workflow Claude Code praktis dan adopsi tim.