ze-feedback
A lightweight, type-safe feedback widget for React apps. Built with Radix UI, Zod, and Tailwind v4.
Why use it?
- Type-safe payload with Zod validation
- Accessible dialog (Radix UI)
- Optional 1–5 star rating
- Tiny API, sensible defaults, themeable
- CSS auto-included (no extra imports)
- Dashboard component to view all feedback
- Dashboard button to navigate to feedback dashboard
Installation
npm install ze-feedback
npm install react react-dom
Quick start
import { FeedbackWidget } from "ze-feedback";
import "ze-feedback/styles.css";
export default function App() {
return <FeedbackWidget apiUrl="/api/feedback" />;
}
The styles must be imported from ze-feedback/styles.css.
Components
FeedbackWidget
The main feedback widget that displays a trigger button and handles feedback submission.
ZeDashboard
A full-page dashboard component to view all feedback submissions with ratings, messages, and metadata.
ZeDashboardButton
A button component that navigates to your feedback dashboard page.
FeedbackWidget Props
type Theme = "light" | "dark";
interface FeedbackWidgetProps {
apiUrl?: string;
userId?: string;
metadata?: Record<string, any>;
onSuccess?: () => void;
onError?: (err: Error) => void;
theme?: Theme;
renderToast?: (info: {
type: "success" | "error";
message: string;
}) => React.ReactNode;
buttonVariant?: "standAlone" | "simple";
buttonIcon?: React.ReactNode;
onSubmit?: (data: FeedbackPayload) => Promise<void> | void;
validateWith?: ZodTypeAny;
}
ZeDashboard Props
interface ZeDashboardProps {
apiUrl: string;
theme?: Theme;
title?: string;
backRoute?: string;
onBack?: () => void;
}
ZeDashboardButton Props
interface ZeDashboardButtonProps {
routePath?: string;
theme?: Theme;
variant?: ButtonVariant;
icon?: React.ReactNode;
children?: React.ReactNode;
onClick?: () => void;
className?: string;
}
Behavior
- Clicking the trigger opens an accessible modal titled "Send Feedback".
- The form contains a textarea and an optional 1–5 star rating.
- On submit:
- Payload is validated with Zod.
- A POST request is sent to
apiUrl (or custom onSubmit handler is called).
- On success: dialog closes immediately, a short success toast appears,
onSuccess is called.
- On failure: an error toast appears,
onError is called with the Error instance.
- The payload automatically includes
createdAt (ISO string), plus any userId/metadata you provide.
Payload shape (sent to apiUrl)
{
message: string;
rating?: number;
userId?: string;
metadata?: Record<string, any>;
}
Feedback item shape (returned from GET endpoint)
{
id?: string;
message: string;
rating?: number;
userId?: string;
metadata?: Record<string, any>;
createdAt?: string;
}
You can also import the schema and types:
import { feedbackPayloadSchema, ratingSchema } from "ze-feedback";
import type {
FeedbackPayload,
Rating,
ZeDashboardProps,
ZeDashboardButtonProps,
} from "ze-feedback";
Examples
Minimal FeedbackWidget
import { FeedbackWidget } from "ze-feedback";
import "ze-feedback/styles.css";
<FeedbackWidget apiUrl="/api/feedback" />;
With metadata and hooks
<FeedbackWidget
apiUrl="/api/feedback"
userId="user-123"
metadata={{ page: "/dashboard", plan: "pro" }}
onSuccess={() => console.log("Thanks!")}
onError={(e) => console.error(e)}
/>
Dark theme
<FeedbackWidget apiUrl="/api/feedback" theme="dark" />
Custom toast
<FeedbackWidget
apiUrl="/api/feedback"
renderToast={({ type, message }) => (
<div
style={{
position: "fixed",
top: 16,
right: 16,
padding: "10px 14px",
borderRadius: 8,
color: "#fff",
background: type === "success" ? "#16a34a" : "#ef4444",
boxShadow: "0 6px 18px rgba(0,0,0,.2)",
zIndex: 9999,
}}
role="alert"
>
{message}
</div>
)}
/>
Button variants
<FeedbackWidget apiUrl="/api/feedback" />
<FeedbackWidget
apiUrl="/api/feedback"
buttonVariant="simple"
buttonIcon={<YourIcon className="w-4 h-4" />}
/>
Custom submit handler
<FeedbackWidget
onSubmit={async (data) => {
await fetch("/api/feedback", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
}}
/>
ZeDashboard with React Router
import { ZeDashboard } from "ze-feedback";
import { useNavigate } from "react-router-dom";
function DashboardPage() {
const navigate = useNavigate();
return (
<ZeDashboard
apiUrl="http://localhost:5005/api/feedback/list"
onBack={() => navigate("/")}
title="Feedback Dashboard"
theme="light"
/>
);
}
ZeDashboard with simple routing
import { ZeDashboard } from "ze-feedback";
<ZeDashboard
apiUrl="http://localhost:5005/api/feedback/list"
backRoute="/"
title="Feedback Dashboard"
/>;
ZeDashboardButton with React Router
import { ZeDashboardButton } from "ze-feedback";
import { useNavigate } from "react-router-dom";
function App() {
const navigate = useNavigate();
return (
<ZeDashboardButton
onClick={() => navigate("/ze-dashboard")}
theme="light"
variant="simple"
/>
);
}
ZeDashboardButton with simple routing
import { ZeDashboardButton } from "ze-feedback";
<ZeDashboardButton routePath="/ze-dashboard" theme="light" />;
Complete example with React Router
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { FeedbackWidget, ZeDashboard, ZeDashboardButton } from "ze-feedback";
import { useNavigate } from "react-router-dom";
import "ze-feedback/styles.css";
function App() {
const navigate = useNavigate();
return (
<div>
<FeedbackWidget apiUrl="http://localhost:5005/api/feedback" />
<ZeDashboardButton onClick={() => navigate("/ze-dashboard")} />
</div>
);
}
function DashboardPage() {
const navigate = useNavigate();
return (
<ZeDashboard
apiUrl="http://localhost:5005/api/feedback/list"
onBack={() => navigate("/")}
/>
);
}
export default function Root() {
return (
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/ze-dashboard" element={<DashboardPage />} />
</Routes>
</BrowserRouter>
);
}
Backend Setup
Flask Backend Example
Create a Python backend with Flask:
from flask import Flask, request, jsonify
from flask_cors import CORS
from datetime import datetime
import uuid
app = Flask(__name__)
CORS(app)
feedback_list = []
@app.route("/api/feedback", methods=["POST"])
def receive_feedback():
"""
Endpoint to receive feedback from the widget
"""
try:
feedback_data = request.get_json()
feedback_item = {
"id": str(uuid.uuid4()),
"message": feedback_data.get("message", ""),
"rating": feedback_data.get("rating"),
"userId": feedback_data.get("userId"),
"metadata": feedback_data.get("metadata", {}),
"createdAt": datetime.utcnow().isoformat() + "Z",
}
feedback_list.append(feedback_item)
return jsonify({
"success": True,
"message": "Feedback received successfully!",
"data": feedback_item
}), 200
except Exception as e:
return jsonify({
"success": False,
"error": str(e)
}), 400
@app.route("/api/feedback/list", methods=["GET"])
def get_feedback_list():
"""
Endpoint to get the list of feedback for the dashboard
Returns feedback sorted by newest first
"""
sorted_feedback = sorted(
feedback_list,
key=lambda x: x.get("createdAt", ""),
reverse=True
)
return jsonify({
"success": True,
"data": sorted_feedback
}), 200
@app.route("/api/ping", methods=["GET"])
def ping():
"""
Health check endpoint
"""
return jsonify({
"status": "ok",
"message": "Backend is running!"
}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5005, debug=True)
Install Flask:
pip install flask flask-cors
Run the server:
python app.py
The server will start on http://localhost:5005
Express.js Backend Example
import express from "express";
import cors from "cors";
import { feedbackPayloadSchema } from "ze-feedback";
const app = express();
app.use(cors());
app.use(express.json());
const feedbackList: FeedbackItem[] = [];
app.post("/api/feedback", async (req, res) => {
const result = feedbackPayloadSchema.safeParse(req.body);
if (!result.success) {
return res
.status(400)
.json({ error: "Invalid feedback data", details: result.error.errors });
}
const feedbackItem = {
id: crypto.randomUUID(),
...result.data,
createdAt: new Date().toISOString(),
};
feedbackList.push(feedbackItem);
return res.json({ success: true, data: feedbackItem });
});
app.get("/api/feedback/list", (req, res) => {
const sortedFeedback = feedbackList.sort(
(a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
);
return res.json({ success: true, data: sortedFeedback });
});
app.listen(5005, () => {
console.log("Server running on http://localhost:5005");
});
Required Endpoints
POST /api/feedback
Receives feedback submissions from the FeedbackWidget.
Request Body:
{
"message": "Great app!",
"rating": 5,
"userId": "user-123",
"metadata": { "page": "/dashboard" }
}
Response:
{
"success": true,
"message": "Feedback received successfully!",
"data": {
"id": "uuid-here",
"message": "Great app!",
"rating": 5,
"userId": "user-123",
"metadata": { "page": "/dashboard" },
"createdAt": "2024-01-01T12:00:00Z"
}
}
GET /api/feedback/list
Returns all feedback for the dashboard component.
Response:
{
"success": true,
"data": [
{
"id": "uuid-here",
"message": "Great app!",
"rating": 5,
"userId": "user-123",
"metadata": { "page": "/dashboard" },
"createdAt": "2024-01-01T12:00:00Z"
}
]
}
The dashboard component expects either:
- An array directly:
[...]
- An object with
data property: { "data": [...] }
- An object with
feedback property: { "feedback": [...] }
Notes
- This package treats
react and react-dom as peer dependencies.
- When developing locally via
npm link with Vite/Next:
- Make sure there is only one copy of React loaded.
- In Vite, set
resolve.dedupe = ['react','react-dom'].
- In Next, set
transpilePackages: ['ze-feedback'].
- The dashboard component automatically handles different response formats and sorts feedback by newest first.
- All components support light and dark themes via the
theme prop.
License
MIT