@villetorio/lms-feedback-widget
v1.0.0
Published
Floating feedback widget — framework agnostic, works in Svelte, SvelteKit, Sapper, React, Vue, plain HTML
Maintainers
Readme
@villetorio/lms-feedback-widget
Framework-agnostic floating feedback widget. Works in Sapper, SvelteKit, Next.js, React, Vue, and plain HTML — zero framework dependencies.
Features
- ⭐ Star rating (1–5)
- 💬 Optional message textarea
- ✅ Success / error states
- ♿ Accessible (ARIA labels, keyboard support)
- 🌙 Click-outside to close
- 📦 Zero framework dependencies — pure TypeScript
- 🔌 Works in any JS project
Installation
npm install @villetorio/lms-feedback-widgetFor projects with peer dependency conflicts (e.g. old Svelte 3 / Rollup 1):
npm install @villetorio/lms-feedback-widget --legacy-peer-depsUsage
⚠️ Always call
FeedbackWidget()inside a mount/effect hook — never at the top level — because it needsdocument.bodyto exist first.
Sapper
<!-- src/routes/_layout.svelte -->
<script>
import { onMount } from 'svelte';
import { FeedbackWidget } from '@villetorio/lms-feedback-widget';
onMount(() => {
FeedbackWidget({ apiRoute: '/api/feedback', appName: 'My App' });
});
</script>
<slot />API endpoint:
// src/routes/api/feedback.js
export async function post(req, res) {
const payload = req.body;
console.log('[Feedback]', payload);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
}SvelteKit
<!-- src/routes/+layout.svelte -->
<script>
import { onMount } from 'svelte';
import { FeedbackWidget } from '@villetorio/lms-feedback-widget';
onMount(() => {
FeedbackWidget({ apiRoute: '/api/feedback', appName: 'My App' });
});
</script>
<slot />API endpoint:
// src/routes/api/feedback/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async ({ request }) => {
const payload = await request.json();
console.log('[Feedback]', payload);
return json({ ok: true });
};Next.js (App Router)
// src/components/FeedbackWidgetLoader.tsx
'use client';
import { useEffect } from 'react';
import { FeedbackWidget } from '@villetorio/lms-feedback-widget';
export default function FeedbackWidgetLoader() {
useEffect(() => {
FeedbackWidget({ apiRoute: '/api/feedback', appName: 'My App' });
}, []);
return null;
}// src/app/layout.tsx
import FeedbackWidgetLoader from '@/components/FeedbackWidgetLoader';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<FeedbackWidgetLoader />
</body>
</html>
);
}API endpoint:
// src/app/api/feedback/route.ts
import { NextRequest, NextResponse } from 'next/server';
export async function POST(req: NextRequest) {
const payload = await req.json();
console.log('[Feedback]', payload);
return NextResponse.json({ ok: true });
}React
// FeedbackWidgetLoader.tsx
import { useEffect } from 'react';
import { FeedbackWidget } from '@villetorio/lms-feedback-widget';
export default function FeedbackWidgetLoader() {
useEffect(() => {
FeedbackWidget({ apiRoute: '/api/feedback', appName: 'My App' });
}, []);
return null;
}// App.tsx
import FeedbackWidgetLoader from './FeedbackWidgetLoader';
export default function App() {
return (
<>
<YourRoutes />
<FeedbackWidgetLoader />
</>
);
}Vue
<!-- App.vue or a layout component -->
<script setup>
import { onMounted } from 'vue';
import { FeedbackWidget } from '@villetorio/lms-feedback-widget';
onMounted(() => {
FeedbackWidget({ apiRoute: '/api/feedback', appName: 'My App' });
});
</script>
<template>
<RouterView />
</template>Plain HTML (no bundler)
<script src="node_modules/@villetorio/lms-feedback-widget/dist/index.umd.js"></script>
<script>
LMSFeedbackWidget.FeedbackWidget({
apiRoute: '/api/feedback',
appName: 'My App'
});
</script>Or via CDN:
<script src="https://unpkg.com/@villetorio/lms-feedback-widget/dist/index.umd.js"></script>
<script>
LMSFeedbackWidget.FeedbackWidget({ apiRoute: '/api/feedback', appName: 'My App' });
</script>Options
| Option | Type | Default | Description |
|---|---|---|---|
| apiRoute | string | "/api/feedback" | URL to POST feedback payload to |
| apiKey | string | "" | Optional Bearer token for Authorization header |
| title | string | "Share Your Feedback" | Panel heading |
| subtitle | string | "Help us improve" | Panel subheading |
| appName | string | "App" | Included in payload and success message |
| onSuccess | (payload: FeedbackPayload) => void | undefined | Called after successful submission |
| onError | (error: Error) => void | undefined | Called when submission fails |
Payload Shape
The widget POSTs this JSON to your apiRoute:
interface FeedbackPayload {
rating: number; // 1–5 stars
message: string; // optional user text
app: string; // from appName option
timestamp: string; // ISO 8601 e.g. "2026-03-04T07:00:00.000Z"
url: string; // current page URL
}Mount Hook Reference
| Framework | Hook |
|---|---|
| Svelte / Sapper | onMount(() => { FeedbackWidget({...}) }) |
| SvelteKit | onMount(() => { FeedbackWidget({...}) }) |
| React / Next.js | useEffect(() => { FeedbackWidget({...}) }, []) |
| Vue | onMounted(() => { FeedbackWidget({...}) }) |
| Plain HTML | Bottom of <body> or DOMContentLoaded |
Build Outputs
| File | Format | Use case |
|---|---|---|
| dist/index.js | ESM | SvelteKit, Vite, modern bundlers |
| dist/index.cjs | CJS | Node.js, Sapper, Rollup 1, Jest |
| dist/index.umd.js | UMD | Plain <script> tag, CDN |
