Alerts, forms and toasts that open from where you clicked.
Popet replaces the browser's alert boxes with dialogs your users can read, answer and fill in. One file, no dependencies, and it fits React, Vue, Angular and Laravel.
Try it first
Each tile opens a real popup. Notice where it grows from, and where it goes back to.
Examples
Pick one, read the code, run it. What you see is exactly what runs.
Inputs
Every field is drawn by Popet, so it looks the same in every browser: a calendar and a clock, searchable and multiple selects, a drag-and-drop file zone, sliders, checkboxes, radios and switches.
Icons
Toasts
Small notices that stack, pause while you hover, and leave on their own. Four styles, five tones, nine positions.
Styles
Tones
Tones use the style you clicked last.
Positions
In code
Popet.toast({
variant: 'solid', // 'soft' (default), 'solid', 'outline' or 'minimal'
icon: 'success',
title: 'Success',
text: 'JOHN DOE disconnected successfully',
showCloseButton: true,
position: 'top-end'
})
// Make one style the default for every toast
Popet.setDefaults({ variant: 'solid' })
Install and use
Import it, then call it once the page has loaded.
1. Import the plugin
npm install popetjs
import Popet from 'popetjs'
// or via CommonJS
const Popet = require('popetjs')
The default import brings its own styles. To manage the stylesheet yourself, import them separately:
import Popet from 'popetjs/core' import 'popetjs/css'
2. Call it
Popet.fire({
title: 'Error!',
text: 'Do you want to continue?',
icon: 'error',
confirmButtonText: 'Cool'
})
Or the old-school way, from a CDN
<script src="https://cdn.jsdelivr.net/npm/popetjs@1/dist/popet.all.min.js"></script>
With the stylesheet separately, if you prefer:
<script src="https://cdn.jsdelivr.net/npm/popetjs@1/dist/popet.min.js"></script> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/popetjs@1/dist/popet.min.css">
The font
Popet is set in Poppins. Load it on your page and the popups pick it up; without it they fall back to the system font. To use your own, set --popet-font.
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;500;600;700&display=swap">
Integrations with major frameworks
Popet is plain JavaScript, so the import above already works everywhere. These adapters add what each framework expects.
Import from popetjs/react and the content options accept JSX.
import Popet, { usePopet } from 'popetjs/react'
function DeleteButton({ user }) {
const popet = usePopet()
async function remove() {
const ok = await popet.confirm({
icon: 'warning',
title: <>Delete <strong>{user.name}</strong>?</>,
confirmButtonText: 'Delete'
})
if (ok) popet.toast('User deleted', 'success')
}
return <button onClick={remove}>Delete</button>
}
Install the plugin once, then use $popet in templates or usePopet() in setup.
// main.js
import { createApp } from 'vue'
import { PopetPlugin } from 'popetjs/vue'
createApp(App).use(PopetPlugin, { theme: 'auto' }).mount('#app')
<script setup>
import { h } from 'vue'
import { usePopet } from 'popetjs/vue'
import PlanPicker from './PlanPicker.vue'
const popet = usePopet()
const choose = () => popet.fire({ title: 'Choose a plan', html: h(PlanPicker) })
</script>
<template>
<button @click="$popet.success('Saved!')">Save</button>
<button @click="choose">Change plan</button>
</template>
Inject the POPET token anywhere. providePopet() is optional and sets app-wide defaults.
// app.config.ts
import { providePopet } from 'popetjs/angular'
export const appConfig = {
providers: [providePopet({ theme: 'auto' })]
}
// invoice.component.ts
import { Component, inject } from '@angular/core'
import { POPET } from 'popetjs/angular'
@Component({
selector: 'app-invoice',
template: `<button (click)="send()">Send</button>`
})
export class InvoiceComponent {
private popet = inject(POPET)
async send() {
if (await this.popet.confirm('Send this invoice?')) {
this.popet.toast('Invoice sent', 'success')
}
}
}
Flash a popup from a controller and it appears on the next page. Put @popet once before </body> in your layout.
composer require popet/laravel php artisan vendor:publish --tag=popet-assets
// app/Http/Controllers/ProfileController.php
use Popet\Laravel\Facades\Popet;
public function update(Request $request)
{
$request->user()->update($request->validated());
Popet::toast('Profile updated'); // a toast
// Popet::success('Saved', 'Your profile is live.'); // or a modal
return back();
}
{{-- resources/views/layouts/app.blade.php --}}
@popet
</body>
Livewire components can open one without a page load:
$this->dispatch('popet', title: 'Saved', icon: 'success', toast: true);
Forms
Pass fields and the popup becomes a form. Popet.form() adds a title bar and a button bar, and resolves the values, or null when cancelled.
Columns and rows
Popet.form({
title: 'New customer',
columns: 2, // fields per line
fields: [
{ name: 'first', label: 'First name', required: true },
{ name: 'last', label: 'Last name' },
{ name: 'notes', label: 'Notes', type: 'textarea', col: 12 },
{ type: 'divider' },
// an array is a row: equal widths
[
{ name: 'city', label: 'City' },
{ name: 'zip', label: 'Postcode' },
{ name: 'joined', label: 'Joined', type: 'date' }
]
]
})
The grid has 12 tracks. columns sets the default width and col overrides it for one field: col: 6 is half, col: 12 is the full width. On phones every field takes the full width.
Validation
{
name: 'username',
label: 'Username',
required: true,
validate: async (value, values) => {
if (value.length < 6) return 'Use at least 6 characters'
if (await isTaken(value)) return 'Already taken'
return { success: 'Username is available' }
}
}
A field turns red with its message when it is wrong and green when it is right. Fields are checked as you leave them and again on submit. To show errors from your server, throw them from preConfirm in Laravel's shape:
throw { errors: { phone: ['Already registered'] } }
Field properties
| Property | What it does |
|---|
Form controller
Passed to onFieldChange(name, value, form) and returned by Popet.getForm() while a form is open.
| Method | What it does |
|---|
Options
Everything you can pass to Popet.fire(). All of it is optional.
| Option | Default | What it does |
|---|
Handling the result
Popet.fire() returns a promise that resolves when the popup closes. It never rejects.
const result = await Popet.fire({
title: 'Save your changes?',
showDenyButton: true,
showCancelButton: true
})
if (result.isConfirmed) save()
else if (result.isDenied) discard()
else console.log(result.dismiss) // 'cancel', 'esc', ...
| Key | Meaning |
|---|---|
isConfirmed | The confirm button was used. |
isDenied | The deny button was used. |
isDismissed | It was closed any other way. |
value | The input value, the form values, or whatever preConfirm returned. |
dismiss | Why it was dismissed: cancel, backdrop, close, esc, timer or replaced. |
Methods
Shortcuts for the common cases, and controls for the popup that is open right now.
| Method | What it does |
|---|
Themes
Popet is styled with CSS variables. Choose a look below; the popups and this page change together, and the CSS you need is printed underneath.
Dark mode
Pick 'light', 'dark', or 'auto' to follow the visitor's system setting.
Popet.setDefaults({ theme: 'auto' })