Multi-Option Poll / Survey
Shortcode-based front-end vote block that tallies votes via AJAX and shows live result percentages.
Module3 parts · by SnipCraft
PHPLogic
php
<?php
if ( ! function_exists( 'scseed_poll_shortcode' ) ) {
function scseed_poll_shortcode( $atts ) {
$atts = shortcode_atts( array(
'id' => 'poll1',
'question' => 'What is your favourite feature?',
'options' => 'Option A,Option B,Option C',
), $atts, 'simple_poll' );
$poll_id = preg_replace( '/[^a-z0-9_-]/', '', strtolower( $atts['id'] ) );
$question = esc_html( $atts['question'] );
$options = array_map( 'trim', explode( ',', $atts['options'] ) );
$options = array_slice( $options, 0, 10 );
$voted = isset( $_COOKIE[ 'scseed_poll_' . $poll_id ] );
$counts = array();
$total = 0;
foreach ( $options as $i => $opt ) {
$cnt = (int) get_option( 'scseed_poll_' . $poll_id . '_' . $i, 0 );
$counts[] = $cnt;
$total += $cnt;
}
ob_start();
?>
<div class="scseed-poll-wrap" data-id="<?php echo esc_attr( $poll_id ); ?>" data-nonce="<?php echo esc_attr( wp_create_nonce( 'scseed_poll' ) ); ?>" data-ajax="<?php echo esc_url( admin_url( 'admin-ajax.php' ) ); ?>">
<p class="scseed-poll-question"><?php echo $question; ?></p>
<?php if ( $voted ) : ?>
<div class="scseed-poll-results" data-total="<?php echo esc_attr( $total ); ?>">
<?php foreach ( $options as $i => $opt ) :
$pct = $total > 0 ? round( ( $counts[ $i ] / $total ) * 100 ) : 0;
?>
<div class="scseed-poll-bar-row">
<span class="scseed-poll-opt-label"><?php echo esc_html( $opt ); ?></span>
<div class="scseed-poll-bar-track"><div class="scseed-poll-bar" style="width:<?php echo $pct; ?>%"></div></div>
<span class="scseed-poll-pct"><?php echo $pct; ?>%</span>
</div>
<?php endforeach; ?>
<p class="scseed-poll-total"><?php echo esc_html( $total ); ?> votes</p>
</div>
<?php else : ?>
<ul class="scseed-poll-options">
<?php foreach ( $options as $i => $opt ) : ?>
<li><button type="button" class="scseed-poll-choice" data-opt="<?php echo esc_attr( $i ); ?>"><?php echo esc_html( $opt ); ?></button></li>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<?php
return ob_get_clean();
}
add_shortcode( 'simple_poll', 'scseed_poll_shortcode' );
}
if ( ! function_exists( 'scseed_poll_vote' ) ) {
function scseed_poll_vote() {
check_ajax_referer( 'scseed_poll', 'nonce' );
$poll_id = preg_replace( '/[^a-z0-9_-]/', '', sanitize_text_field( wp_unslash( $_POST['poll_id'] ?? '' ) ) );
$opt_idx = absint( $_POST['opt'] ?? 0 );
if ( ! $poll_id ) wp_send_json_error( 'invalid' );
$key = 'scseed_poll_' . $poll_id . '_' . $opt_idx;
$current = (int) get_option( $key, 0 );
update_option( $key, $current + 1, false );
$max = 20;
$results = array();
$total = 0;
for ( $i = 0; $i < $max; $i++ ) {
$c = (int) get_option( 'scseed_poll_' . $poll_id . '_' . $i, 0 );
if ( $c === 0 && $i > $opt_idx ) break;
$results[] = $c;
$total += $c;
}
setcookie( 'scseed_poll_' . $poll_id, '1', time() + 30 * DAY_IN_SECONDS, COOKIEPATH, COOKIE_DOMAIN );
wp_send_json_success( array( 'results' => $results, 'total' => $total ) );
}
add_action( 'wp_ajax_scseed_poll_vote', 'scseed_poll_vote' );
add_action( 'wp_ajax_nopriv_scseed_poll_vote', 'scseed_poll_vote' );
}CSSStyles
css
.scseed-poll-wrap {
max-width: 480px;
margin: 1.5rem auto;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 1.5rem 1.75rem;
font-family: inherit;
}
.scseed-poll-question { font-size: 1.05rem; font-weight: 700; color: #111827; margin: 0 0 1rem; }
.scseed-poll-options { list-style: none; padding: 0; margin: 0; display: flex; flex-direction: column; gap: .5rem; }
.scseed-poll-choice {
width: 100%;
text-align: left;
background: #f3f4f6;
border: 2px solid transparent;
border-radius: 8px;
padding: .65rem 1rem;
cursor: pointer;
font-size: .95rem;
transition: border-color .15s, background .15s;
}
.scseed-poll-choice:hover { border-color: #6366f1; background: #eff6ff; }
.scseed-poll-bar-row { display: flex; align-items: center; gap: .6rem; margin-bottom: .6rem; }
.scseed-poll-opt-label { flex: 0 0 120px; font-size: .85rem; color: #374151; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.scseed-poll-bar-track { flex: 1; background: #e5e7eb; border-radius: 999px; height: 10px; overflow: hidden; }
.scseed-poll-bar { height: 100%; background: #6366f1; border-radius: 999px; transition: width .8s ease; }
.scseed-poll-pct { font-size: .8rem; font-weight: 600; color: #4b5563; min-width: 30px; text-align: right; }
.scseed-poll-total { font-size: .78rem; color: #9ca3af; margin-top: .3rem; }JavaScriptScript
js
(function () {
document.querySelectorAll('.scseed-poll-wrap').forEach(function (wrap) {
wrap.querySelectorAll('.scseed-poll-choice').forEach(function (btn) {
btn.addEventListener('click', function () {
var pollId = wrap.dataset.id;
var opt = btn.dataset.opt;
var nonce = wrap.dataset.nonce;
var ajax = wrap.dataset.ajax;
wrap.querySelectorAll('.scseed-poll-choice').forEach(function (b) { b.disabled = true; });
var body = new URLSearchParams();
body.append('action', 'scseed_poll_vote');
body.append('poll_id', pollId);
body.append('opt', opt);
body.append('nonce', nonce);
fetch(ajax, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: body.toString() })
.then(function (r) { return r.json(); })
.then(function (data) {
if (!data.success) return;
var results = data.data.results;
var total = data.data.total;
var opts = wrap.querySelectorAll('.scseed-poll-choice');
var html = '<div class="scseed-poll-results">';
results.forEach(function (cnt, i) {
var pct = total > 0 ? Math.round((cnt / total) * 100) : 0;
var label = opts[i] ? opts[i].textContent : ('Option ' + (i + 1));
html += '<div class="scseed-poll-bar-row">' +
'<span class="scseed-poll-opt-label">' + label + '</span>' +
'<div class="scseed-poll-bar-track"><div class="scseed-poll-bar" style="width:' + pct + '%"></div></div>' +
'<span class="scseed-poll-pct">' + pct + '%</span></div>';
});
html += '<p class="scseed-poll-total">' + total + ' votes</p></div>';
wrap.querySelector('.scseed-poll-options').outerHTML = html;
});
});
});
});
})();#ajax#interactive#poll#survey#vote