Create polls with multiple options, enforce one vote per IP or logged-in user, and display live results as an animated bar chart - all without a page reload.
- Unlimited options per poll (2–20)
- One vote per user - logged-in users tracked by user ID; guests tracked by IP address
- Live bar chart - results update instantly after voting via Alpine.js +
fetch, no page reload - Flexible result visibility - show results always, only after voting, or only after the poll closes
- Auto-close - set an optional end date/time; the poll stops accepting votes automatically
- Guest voting toggle - optionally restrict voting to logged-in users only
- Admin results view - full breakdown with vote counts and percentages
- Embeddable - drop any poll into any page, post, or widget area with one line
- Status control - Draft / Active / Closed
- An admin creates a poll in Tools → Polls → New Poll, adds options, and sets the status to Active.
- The poll is embedded anywhere in a theme using
@include('polls::partials.poll', ['pollId' => 1]). - A visitor selects an option and clicks Vote. The vote is sent via
fetchto/polls/{id}/vote. - The voting form is replaced by a bar chart showing results in real time.
- If the visitor revisits the page, the bar chart is shown immediately (the plugin checks their IP / user ID on page load).
- Logged-in users - enforced by a unique database constraint on
(poll_id, user_id). A second request hits the constraint and returns an error. - Guests - checked by IP address before inserting. One IP = one vote per poll.
- A race condition between the IP check and the insert is handled by catching the database unique constraint exception.
Go to Plugins in your Contensio admin, find Simple Polls, and click Install.
composer require contensio/plugin-pollsThe plugin is auto-discovered. Go to Plugins in the admin and enable it. Migrations run automatically on first enable.
@include('polls::partials.poll', ['pollId' => 1])Replace 1 with the poll's ID, which is shown on the polls list screen.
The embedded widget is self-contained - it includes its own Alpine.js component and handles voting, error states, and result rendering without any additional setup.
Requirements for the embed to work:
- Alpine.js must be loaded on the page (included in all Contensio default themes).
- A
<meta name="csrf-token">tag must be present in the page<head>(included in all Contensio default themes).
Shows all polls with their status, total vote count, result visibility setting, and expiry date. From here you can create, edit, view results, or delete a poll.
The embed snippet is shown inline on the list for quick copy-paste.
| Field | Description |
|---|---|
| Question | The poll question (up to 500 characters) |
| Options | Add or remove options dynamically; minimum 2, maximum 20 |
| Status | Draft (hidden), Active (accepting votes), Closed (no more votes) |
| Show results | Always, After voting, or After closing |
| Allow guests | Whether non-logged-in visitors can vote (one vote per IP) |
| Auto-close at | Optional date/time to automatically stop accepting votes |
Displays a bar chart with vote count and percentage per option, total votes, and current status.
| Method | URL | Description |
|---|---|---|
GET |
/account/polls |
Admin poll list |
GET |
/account/polls/create |
New poll form |
POST |
/account/polls |
Create poll |
GET |
/account/polls/{id}/edit |
Edit poll |
PUT |
/account/polls/{id} |
Update poll |
DELETE |
/account/polls/{id} |
Delete poll + all votes |
GET |
/account/polls/{id}/results |
Admin results view |
POST |
/polls/{id}/vote |
Cast a vote (JSON) |
GET |
/polls/{id}/results |
Fetch current results (JSON) |
Three tables created by the migration:
| Column | Type | Description |
|---|---|---|
id |
bigint | Primary key |
question |
varchar(500) | The poll question |
status |
enum | draft, active, closed |
show_results |
enum | always, after_vote, after_close |
allow_guests |
boolean | Whether guests can vote |
ends_at |
timestamp | Auto-close date/time (nullable) |
created_at / updated_at |
timestamp |
| Column | Type | Description |
|---|---|---|
id |
bigint | Primary key |
poll_id |
bigint | FK → polls.id (cascade delete) |
label |
varchar(300) | Option text |
sort_order |
smallint | Display order |
| Column | Type | Description |
|---|---|---|
id |
bigint | Primary key |
poll_id |
bigint | FK → polls.id (cascade delete) |
option_id |
bigint | FK → poll_options.id (cascade delete) |
user_id |
bigint | Logged-in user ID (nullable) |
ip_address |
varchar(45) | Voter IP address |
created_at |
timestamp | Vote time |
Unique constraint on (poll_id, user_id) prevents duplicate votes from logged-in users at the database level.
- PHP 8.2+
- Contensio 2.0+
- Alpine.js (included in all Contensio default themes)
AGPL-3.0-or-later - see LICENSE.