If you have a WordPress site running a bbPress forum and you want to add user voting — up votes, down votes, and smart score sorting — then you’ve come to the right place.
As an experienced WordPress developer and the developer of the free bbPress Voting plugin, I’ve built this from scratch dozens of times. In this guide, I’ll walk you through exactly how it works under the hood so you can build it yourself, or understand the architecture before installing the plugin.
In this article, I share the full code architecture for adding voting to bbPress topics and replies — the buttons, AJAX handler, and server-side processing. If you just want voting working on your forum today with zero coding, the free bbPress Voting plugin handles all of this out of the box.
How bbPress Voting Works: The Architecture
Before diving into code, let’s talk about how voting actually works in a bbPress context. There are three core pieces:
- Vote buttons — rendered inline with topics and replies
- Vote tracking — stored as post meta, keyed by user ID (logged in) or IP address (guests)
- Score calculation — a simple count (ups − downs) or a weighted formula like Wilson Score
The tricky part is that bbPress renders topics and replies in multiple contexts — the topic list, the single topic page, and inside reply threads — so the voting buttons need to hook into several places simultaneously.
Adding the Voting Up & Down Buttons
We hook into three bbPress action hooks — two for topics (before the title and before the content) and one for replies. All three call the same function, which figures out which context it’s in and renders the appropriate HTML.
add_action('bbp_theme_before_topic_title', 'wpftw_bbp_voting_buttons');
add_action('bbp_theme_before_topic_content', 'wpftw_bbp_voting_buttons');
add_action('bbp_theme_before_reply_content', 'wpftw_bbp_voting_buttons');
function wpftw_bbp_voting_buttons() {
$topic_post_type = bbp_get_topic_post_type();
$reply_post_type = bbp_get_reply_post_type();
if (current_action() === 'bbp_theme_before_topic_title' || current_action() === 'bbp_theme_before_topic_content') {
$this_post_type = $topic_post_type;
}
if (current_action() === 'bbp_theme_before_reply_content') {
$this_post_type = bbp_get_reply_post_type();
}
// Get the post object from the appropriate query
if ($this_post_type === $topic_post_type) {
$post = bbpress()->topic_query->post ?? null;
} elseif ($this_post_type === $reply_post_type) {
$post = bbpress()->reply_query->post ?? null;
}
if (empty($post)) return;
$post_id = (int) $post->ID;
$ups = (int) get_post_meta($post_id, 'bbp_voting_ups', true);
$downs = (int) get_post_meta($post_id, 'bbp_voting_downs', true);
$score = $ups - $downs;
$voting_log = get_post_meta($post_id, 'bbp_voting_log', true);
$voting_log = is_array($voting_log) ? $voting_log : array();
$identifier = is_user_logged_in() ? (string) get_current_user_id() : $_SERVER['REMOTE_ADDR'];
$existing_vote = isset($voting_log[$identifier]) ? (int) $voting_log[$identifier] : 0;
$classes = array('wpftw-bbp-voting', 'wpftw-bbp-voting-post-' . $post_id);
if ($existing_vote === 1) $classes[] = 'voted-up';
if ($existing_vote === -1) $classes[] = 'voted-down';
printf(
'',
esc_attr(implode(' ', $classes)),
$ups > 0 ? '+' . $ups : '',
$post_id,
$score,
$downs > 0 ? (string) $downs : '',
$post_id
);
}Style the Voting Buttons with SCSS
This SCSS compiles down to CSS for styling the up/down arrows — green on up vote hover, red on down vote hover, just like Stack Overflow. The score sits in the middle in bold, neutral gray.
$green: #1e851e;
$red: #992121;
$neutral: #858c93;
.wpftw-bbp-voting {
margin: 8px 0;
padding-right: 30px;
min-width: 65px;
float: left;
.score {
font-size: 25px;
font-weight: bold;
text-align: center;
color: $neutral;
padding: 2px 0 3px;
}
a.vote {
display: block;
position: relative;
overflow: visible;
text-indent: -9999em;
width: 0;
height: 0;
margin: 3px auto;
border-left: 15px solid transparent;
border-right: 15px solid transparent;
cursor: pointer;
&.up { border-bottom: 15px solid $neutral; &:hover { border-bottom-color: $green; } }
&.down { border-top: 15px solid $neutral; &:hover { border-top-color: $red; } }
&::after {
content: attr(data-votes);
display: block;
position: absolute;
top: -10px;
left: 17px;
text-indent: 0;
color: #aaa;
opacity: 0;
transition: opacity 500ms ease;
}
&:hover::after { opacity: 1; }
}
&.voted-up a.vote.up { border-bottom-color: $green; }
&.voted-down a.vote.down { border-top-color: $red; }
}AJAX Handler for Vote Clicks
Now we wire up the frontend. When a user clicks Up or Down, JavaScript sends an AJAX request to WordPress. Here’s the enqueue setup and the JS handler:
add_action('wp_enqueue_scripts', 'wpftw_bbp_voting_scripts');
function wpftw_bbp_voting_scripts() {
wp_enqueue_script(
'wpftw-bbp-voting-js',
plugin_dir_url(__FILE__) . 'wpftw-bbp-voting.js',
array('jquery'),
filemtime(plugin_dir_path(__FILE__) . 'wpftw-bbp-voting.js')
);
wp_localize_script(
'wpftw-bbp-voting-js',
'wpftw_bbp_voting_ajax_object',
array('ajax_url' => admin_url('admin-ajax.php'))
);
}function wpftw_bbpress_post_vote_link_clicked(post_id, direction) {
var $wrapper = jQuery('.wpftw-bbp-voting.wpftw-bbp-voting-post-' + post_id);
$wrapper.css({ opacity: 0.5, 'pointer-events': 'none' });
jQuery.post(wpftw_bbp_voting_ajax_object.ajax_url, {
action: 'wpftw_bbpress_post_vote_link_clicked',
post_id: post_id,
direction: direction,
nonce: wpftw_bbp_voting_ajax_object.nonce
}, function(response) {
if (!response || typeof response.score === 'undefined') {
console.log('Voting error:', response);
$wrapper.css({ opacity: 1, 'pointer-events': 'auto' });
return;
}
var score = parseInt(response.score, 10);
var ups = parseInt(response.ups, 10);
var downs = parseInt(response.downs, 10);
var dir = parseInt(response.direction, 10);
$wrapper.find('.score').text(score);
$wrapper.find('.up').attr('data-votes', ups > 0 ? '+' + ups : '');
$wrapper.find('.down').attr('data-votes', downs > 0 ? '' + downs : '');
$wrapper
.removeClass('voted-up voted-down')
.addClass(dir === 1 ? 'voted-up' : dir === -1 ? 'voted-down' : '')
.find(dir === 1 ? '.up' : '.down')
.css(dir === 1 ? 'border-bottom-color' : 'border-top-color',
dir === 1 ? '#1e851e' : '#992121');
$wrapper.css({ opacity: 1, 'pointer-events': 'auto' });
});
}Server-Side Vote Processing
The PHP AJAX handler does the heavy lifting: records the vote, updates post meta, and returns the new score to the browser. It handles three scenarios — new vote, vote removal (clicking the same direction again), and vote reversal (switching from up to down).
add_action('wp_ajax_wpftw_bbpress_post_vote_link_clicked', 'wpftw_bbpress_post_add_vote');
add_action('wp_ajax_nopriv_wpftw_bbpress_post_vote_link_clicked', 'wpftw_bbpress_post_add_vote');
function wpftw_bbpress_post_add_vote() {
header('Content-Type: application/json');
$post_id = (int) ($_POST['post_id'] ?? 0);
$direction = (int) ($_POST['direction'] ?? 0);
if (!in_array($direction, array(1, -1), true)) $direction = 0;
$voting_log = get_post_meta($post_id, 'bbp_voting_log', true);
$voting_log = is_array($voting_log) ? $voting_log : array();
$identifier = is_user_logged_in()
? (string) get_current_user_id()
: ($_SERVER['REMOTE_ADDR'] ?? '0');
$remove_vote = isset($voting_log[$identifier]) && (int) $voting_log[$identifier] === $direction;
$reverse_vote = isset($voting_log[$identifier]) && (int) $voting_log[$identifier] === ($direction * -1);
$ups = (int) get_post_meta($post_id, 'bbp_voting_ups', true);
$downs = (int) get_post_meta($post_id, 'bbp_voting_downs', true);
$score = $ups - $downs;
if ($direction === 1) {
if ($remove_vote) { $ups--; $score--; }
elseif ($reverse_vote) { $downs++; $score++; }
else { $ups++; $score++; }
} elseif ($direction === -1) {
if ($remove_vote) { $downs--; $score++; }
elseif ($reverse_vote) { $ups--; $score--; }
else { $downs++; $score--; }
}
update_post_meta($post_id, 'bbp_voting_ups', $ups);
update_post_meta($post_id, 'bbp_voting_downs', $downs);
update_post_meta($post_id, 'bbp_voting_score', $ups - $downs);
$real_direction = $remove_vote ? 0 : $direction;
$voting_log[$identifier] = $real_direction;
update_post_meta($post_id, 'bbp_voting_log', $voting_log);
wp_send_json(array(
'score' => $ups - $downs,
'direction' => $real_direction,
'ups' => $ups,
'downs' => $downs,
));
}Sorting Topics by Score: Why Simple Arithmetic Falls Short
Once you have votes, you’ll want to sort by them. The naive approach — sorting by ups − downs — creates perverse incentives. A post with 100 upvotes and 50 downvotes scores 50, higher than a brand new post with just 10 upvotes and 0 downvotes (score: 10). But the new post has a far better up/down ratio.
The solution is to use a weighted score — specifically the Wilson Score interval. This formula accounts for both the ratio of upvotes to total votes and the confidence of that ratio based on sample size.
/**
* Calculate the Wilson Score interval for a given number of ups and downs.
* Returns a value between 0 and 1. Higher = more confidently positive.
*
* @param int $ups Number of upvotes
* @param int $downs Number of downvotes
* @param float $z Z-score for confidence level (1.64485 ≈ 95%)
* @return float
*/
function wpftw_calculate_wilson_score(int $ups, int $downs, float $z = 1.64485): float {
$n = $ups + $downs;
if ($n === 0) return 0.0;
$p = $ups / $n;
$denominator = 1 + ($z * $z) / $n;
$center = $p + ($z * $z) / (2 * $n);
$spread = $z * sqrt(($p * (1 - $p) + ($z * $z) / (4 * $n)) / $n);
return max(0.0, ($center - $spread) / $denominator);
}Plug in your ups and downs, get a score between 0 and 1. Sorting by this value surfaces genuinely popular content while naturally suppressing low-confidence posts. Here’s how you’d use it in a WP_Meta_Query or custom SQL sort:
// Example: Sort topics by Wilson score descending
$topics = new WP_Query(array(
'post_type' => bbp_get_topic_post_type(),
'posts_per_page' => 20,
'orderby' => 'meta_value_num',
'meta_key' => 'bbp_voting_wilson_score',
));
// Store Wilson score when a vote is cast:
$ups = (int) get_post_meta($post_id, 'bbp_voting_ups', true);
$downs = (int) get_post_meta($post_id, 'bbp_voting_downs', true);
update_post_meta($post_id, 'bbp_voting_wilson_score',
wpftw_calculate_wilson_score($ups, $downs));The Bottom Line
If you’ve followed along this far, you now understand how bbPress voting works at the code level. Pretty cool, huh?
But here’s the reality: building this yourself means maintaining it across WordPress updates, bbPress updates, and PHP version changes. A production implementation needs nonce verification, rate limiting, guest vote caching, CSRF protection, and more — on top of what I’ve shown above.
The free bbPress Voting plugin gives you everything described above, battle-tested and maintained. Install it and your forum has voting the same day.
What bbPress Voting Pro Adds
If you’re running a serious community forum, the free plugin is a great start — but bbPress Voting Pro is where things get genuinely powerful. Here’s what the Pro version adds:
Accepted Answers
bbPress Voting Pro lets topic authors mark a reply as the accepted answer. That reply gets pinned to the top of the thread, visually distinguished, and can be weighted heavily in your sorting algorithm. It’s the single most effective feature for driving helpful responses in a support-style forum.
Wilson Score Sorting — Out of the Box
Rather than implementing Wilson Score yourself, Pro sorts your topic lists using it automatically. Your most confident, popular topics rise to the top — no custom code required.
Trending / Hot Score Sorting
Stack Overflow-style hot sorting balances recency with popularity. A topic that got 20 upvotes in the last 2 hours ranks higher than a topic with 100 upvotes from last year. Keeps your forum front page feeling alive.
Sort Dropdown UI
Pro adds a front-end dropdown letting users pick how they want to sort — newest, most votes, Wilson score, or trending. Just like Reddit or Stack Overflow.
Schema.org Q&A Rich Snippets
Pro outputs JSON-LD structured data so Google shows your forum topics as Q&A results with accepted answer previews — significantly higher CTR than plain blue links.
GamiPress Integration
Pro integrates with GamiPress so votes trigger achievement unlocks and point awards. Users who receive upvotes earn points. Users whose replies are marked accepted answers unlock achievements. It transforms your forum into a gamified community that users want to participate in.
Next Steps
Start with the free bbPress Voting plugin if you just need up/down buttons and basic score tracking. Upgrade to bbPress Voting Pro when you’re ready for Wilson sorting, accepted answers, rich snippets, and GamiPress integration.
If you have questions about the code in this post, drop a comment below. If you need plugin support, head to the WordPress.org support forum. Good luck building your forum!
Related Posts:
Blogger, expert WordPress developer, and developer of the awesome bbPress Voting plugin which is a must-have plugin for any bbPress forum.
Download bbPress Voting for free on the WordPress Plugin Directory.


