Formulir feedback untuk website atau aplikasi6 menit baca
Buat widget feedback mengambang untuk aplikasi React Anda
Tombol feedback mengambang untuk aplikasi React yang membuka panel kecil, mengirim layar, versi aplikasi, dan akun bersama setiap jawaban, dan tidak pernah kehilangan draf.
Render tombol fixed yang membuka panel berbasis Popover API browser, sehingga panel berada di atas setiap lapisan aplikasi Anda tanpa perang z-index dan tertutup saat Escape ditekan. Saat submit, tambahkan route saat ini, versi aplikasi, ID akun, dan viewport ke data formulir, lalu kirim dari browser dengan Ikiform SDK. Kirim email pengguna hanya jika mereka mencentang kotak yang meminta balasan.
Apa yang berbeda dari widget dalam aplikasi
Formulir di halaman menunggu orang menggulir ke sana. Widget ada di setiap layar, yang mengubah apa saja yang bisa salah. Widget harus berada di atas modal, menu, dan toast Anda sendiri. Widget tidak boleh kehilangan pesan yang setengah ditulis saat seseorang mengklik di luarnya. Dan karena pengguna sudah login, Anda sudah tahu siapa mereka dan di mana mereka berada, jadi widget sebaiknya mengirim informasi itu alih-alih menanyakannya.
- Tombol selalu ada di layar, menempel di pojok kanan bawah.
- Mengkliknya membuka panel di top layer browser dan memulai attempt key baru.
- Pengguna memilih wajah dan menulis catatan. State formulir tetap ter-mount selama panel terbuka maupun tersembunyi.
- Saat submit, widget menambahkan route, versi aplikasi, ID akun, dan viewport, lalu mengirim dengan SDK.
- Ikiform menyimpannya sekali, bahkan jika tombol ditekan dua kali, karena kedua kiriman membawa idempotency key yang sama.
- Jika pengguna mengklik di luar atau menekan Escape lebih dulu, panel tersembunyi dan draf masih ada di kesempatan berikutnya.
Field untuk aplikasi
Pratinjau
Menjalankan validator Ikiform di browser Anda. Tidak ada yang dikirim.
may_reply adalah checkbox, disimpan sebagai true atau false.| Field | Berasal dari | Alasan |
|---|---|---|
rating | Pengguna, lima wajah | Field rating dengan max: 5. Wajah lebih cepat dipilih daripada angka |
message | Pengguna | Opsional. Paling berguna saat wajahnya buruk |
may_reply, email | Checkbox, lalu sesi Anda | Email hanya dikirim jika mereka meminta balasan, dan tombol Balas di notifikasi menuju ke mereka |
route | window.location.pathname | Layar yang mana. Dibaca saat submit, setelah navigasi sisi klien apa pun |
app_version | Build Anda | Memberi tahu apakah keluhan menyangkut kode yang sudah Anda ubah |
account_id | Sesi Anda | Memungkinkan Anda melihat paket dan data mereka. Bukan bukti identitas, lihat di bawah |
viewport | window.innerWidth dan innerHeight | Keluhan tata letak sering bergantung pada ukuran |
Komponennya
Komponen ini butuh @ikiform/sdk dan React 19, yang punya tipe untuk popover, popoverTarget, dan event toggle. popoverTarget pada tombol mengikatnya ke panel, sehingga browser menangani pembukaan, Escape, dan pengembalian fokus ke tombol.
import { submit } from "@ikiform/sdk";
import { type FormEvent, type ToggleEvent, useId, useRef, useState } from "react";
import "./feedback-widget.css";
const FORM_ID = "frm_xxxxxxxxxxxxxxxx";
const FACES = ["Awful", "Bad", "Okay", "Good", "Great"];
interface Props {
accountId: string;
email: string;
appVersion: string;
}
export function FeedbackWidget({ accountId, email, appVersion }: Props) {
const panelId = useId();
const attempt = useRef("");
const [status, setStatus] = useState<"idle" | "sending" | "sent">("idle");
const [error, setError] = useState("");
function onToggle(event: ToggleEvent<HTMLDivElement>) {
if (event.newState !== "open") return;
attempt.current = crypto.randomUUID();
if (status === "sent") setStatus("idle");
}
async function onSubmit(event: FormEvent<HTMLFormElement>) {
event.preventDefault();
const data = new FormData(event.currentTarget);
if (data.get("may_reply") === "true") data.set("email", email);
data.set("route", window.location.pathname);
data.set("app_version", appVersion);
data.set("account_id", accountId);
data.set("viewport", `${window.innerWidth}x${window.innerHeight}`);
setStatus("sending");
setError("");
const result = await submit(FORM_ID, data, { idempotencyKey: attempt.current });
if (result.ok) {
setStatus("sent");
return;
}
setError(result.error.fields?.rating?.[0] ?? result.error.message);
setStatus("idle");
}
return (
<>
<button type="button" className="feedback-trigger" popoverTarget={panelId}>
Feedback
</button>
<div id={panelId} popover="auto" className="feedback-panel" onToggle={onToggle} aria-label="Send feedback">
{status === "sent" ? (
<p role="status">Thanks, that helps.</p>
) : (
<form onSubmit={onSubmit}>
<fieldset>
<legend>How is it going?</legend>
{FACES.map((face, index) => (
<label key={face}>
<input type="radio" name="rating" value={index + 1} required /> {face}
</label>
))}
</fieldset>
<label htmlFor={`${panelId}-message`}>Tell us more</label>
<textarea id={`${panelId}-message`} name="message" rows={4} maxLength={4000} />
<label>
<input type="checkbox" name="may_reply" value="true" /> The team can reply to me by email
</label>
<input name="_gotcha" type="text" tabIndex={-1} autoComplete="off" hidden />
{error ? <p role="alert">{error}</p> : null}
<button disabled={status === "sending"}>{status === "sending" ? "Sending" : "Send"}</button>
</form>
)}
</div>
</>
);
}popover="auto"menaruh panel di top layer saat terbuka. Panel tampil di atas dialog dan menu Anda tanpaz-index, dan tertutup saat pengguna mengklik di tempat lain.- Child panel tetap ter-mount saat panel tersembunyi, sehingga draf selamat dari klik tak sengaja di luar. Setelah pengiriman berhasil, baris terima kasih menggantikan formulir, yang mengosongkannya, dan pembukaan berikutnya menampilkan formulir kosong.
onTogglemembuat key baru setiap kali panel dibuka.submit()mengirimnya sebagai headerIdempotency-Key, jadi klik ganda hanya menyimpan satu jawaban. Lihat idempotensi.- Email hanya diisi jika kotaknya dicentang. Jika tidak dicentang, jawaban membawa ID akun tetapi tanpa alamat.
Gaya default popover menaruhnya di tengah layar. Aturan ini menyematkannya di atas tombol:
.feedback-trigger {
position: fixed;
right: 16px;
bottom: 16px;
}
.feedback-panel {
inset: auto 16px 64px auto;
margin: 0;
width: min(360px, calc(100vw - 32px));
max-height: calc(100dvh - 96px);
overflow: auto;
}Mount sekali saja
Render widget di layout yang membungkus layar untuk pengguna yang login, agar muncul di mana-mana dan tetap ada saat navigasi. Di App Router Next.js, layout adalah server component yang membaca sesi dan meneruskan props biasa ke widget klien:
import type { ReactNode } from "react";
import { FeedbackWidget } from "@/components/feedback-widget";
import { getSession } from "@/lib/session";
export default async function AppLayout({ children }: { children: ReactNode }) {
const session = await getSession();
return (
<>
{children}
{session ? (
<FeedbackWidget
accountId={session.accountId}
email={session.email}
appVersion={process.env.NEXT_PUBLIC_APP_VERSION ?? "dev"}
/>
) : null}
</>
);
}Tandai file widget dengan "use client" di App Router. Di aplikasi Vite atau React Router, render di samping router outlet Anda. Cara kerjanya sama.
Apa yang bisa dan tidak bisa dibuktikan browser
Widget ini mengirim langsung dari browser dengan ID publik formulir. Itu kompromi yang tepat untuk feedback: tidak ada server route yang perlu dirawat, dan meta.user_agent milik Ikiform adalah browser asli pengguna. Itu juga berarti siapa pun bisa mengirim account_id apa pun. Gunakan untuk mencari informasi, jangan pernah untuk memberikan akses apa pun.
Pastikan berfungsi
- Di Allowed domains formulir, tambahkan domain situs Anda, ditambah
localhostselama pengembangan. Biarkan kosong untuk menerima kiriman dari mana saja. - Buka panel, ketik catatan, klik di luar, lalu buka lagi. Catatannya masih ada.
- Buka modal di aplikasi Anda, lalu widget. Panel tampil di atas modal.
- Tekan Escape. Panel tertutup dan fokus kembali ke tombol Feedback.
- Pilih wajah dan klik dua kali Kirim. Submissions menampilkan satu jawaban, dengan
route,app_version, danaccount_idterisi. - Kirim sekali dengan kotak balasan dicentang. Tombol Balas di email notifikasi menuju ke pengguna.
Hal yang sering menjebak
- Jauhkan tombol dari elemen mengambang Anda sendiri, seperti gelembung chat atau banner cookie. Pindahkan salah satunya, atau buka widget dari item menu sebagai gantinya.
- Jangan tampilkan tombol di setiap layar checkout atau editor tempat orang sedang berkonsentrasi. Sembunyikan di sana dengan prop.
- Popover API didukung di Chrome, Edge, Firefox, dan Safari versi terbaru. Jika Anda harus mendukung browser lama, periksa dulu sebelum mengandalkannya.
- Setiap field yang ditambahkan lewat kode harus ada di definisi formulir, atau Ikiform menolak kiriman dengan error Unknown field.
- Jangan kirim seluruh state aplikasi. Route, versi, akun, dan ukuran menjawab sebagian besar pertanyaan; selebihnya adalah soal privasi.
Pemecahan masalah
| Masalah | Yang harus dilakukan |
|---|---|
403 origin_not_allowed, atau setiap kiriman masuk ke spam | Domain halaman tidak ada di Allowed domains formulir. Tambahkan, ditambah localhost untuk uji lokal. Subdomain dari domain yang terdaftar diizinkan. |
403 challenge_required | Spam check formulir diatur ke Ikiform dan kiriman fetch tidak membawa token. Ambil token dengan challenge(formId) dari @ikiform/sdk sebelum setiap pengiriman, atau matikan pemeriksaannya. |
| Panel muncul di tengah layar | Stylesheet tidak termuat, jadi gaya popover default browser yang berlaku. Periksa import CSS, atau atur inset dan margin pada panel. |
422 validation_failed | Kiriman tidak cocok dengan definisi field formulir. error.fields menyebutkan setiap field dan alasannya. Samakan name input dengan ID field. |
Pertanyaan
Render tombol fixed dengan popoverTarget yang mengarah ke panel dengan popover=auto. Browser menangani pembukaan, penutupan, dan fokus. Kirim formulir di panel dengan submit() dari @ikiform/sdk dan tambahkan route serta versi aplikasi saat submit.
Popover dirender di top layer, jadi berada di atas dialog Anda sendiri tanpa aturan z-index, dan light dismiss serta Escape sudah tersedia otomatis. Panel feedback tidak perlu memblokir halaman seperti modal.
Tidak. Apa pun yang dikirim dari browser bisa diubah oleh pengguna. Gunakan untuk mencari konteks, dan kirim dari server Anda dengan secret key saat identitas harus bisa dipercaya.
Tidak. Widget mengirim langsung ke Ikiform dari browser, dan Allowed domains membatasi situs mana yang bisa memakai formulir. Tambahkan server route hanya jika Anda butuh identitas yang tepercaya.
Mentok di salah satu langkah, atau ingin panduan untuk situasi Anda? Email hello@ikiform.com.