kartik-v/bootstrap-star-rating
A lightweight jQuery star rating plugin for Bootstrap with fractional star fills, RTL support, customizable sizes, colors, and themes. Includes clear/reset and readonly modes, keyboard accessibility, and a Yii2 widget wrapper for easy integration.
This is not a Laravel package — there is no service provider, no config file, and no Composer install. kartik-v/bootstrap-star-rating is a jQuery plugin (Krajee) that you wire into your Blade views. Treat it as a front-end asset, not a backend dependency.
Install the assets (pick one):
npm install bootstrap-star-rating
// resources/js/app.js
import $ from 'jquery';
window.$ = window.$ || $;
window.jQuery = window.jQuery || $;
import 'bootstrap-star-rating';
import 'bootstrap-star-rating/css/star-rating.css';
import 'bootstrap-star-rating/themes/krajee-fa/theme.css'; // requires Font Awesome
Or skip the build step entirely with the CDN:
@push('styles')
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/css/star-rating.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/themes/krajee-fa/theme.min.css">
@endpush
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/[email protected]/js/star-rating.min.js"></script>
@endpush
First use case — a product/review form with a fractional rating:
<form method="POST" action="{{ route('reviews.store', $product) }}">
@csrf
<input id="rating" name="rating" type="number" step="0.5" min="0" max="5"
value="{{ old('rating', 0) }}" class="rating" required>
@error('rating') <span class="text-danger">{{ $message }}</span> @enderror
<button class="btn btn-primary">Submit</button>
</form>
@push('scripts')
<script>
$(function () {
$('#rating').rating({
min: 0, max: 5, step: 0.5, stars: 5,
showClear: true, showCaption: false,
theme: 'krajee-fa'
});
});
</script>
@endpush
Where to look first: the Krajee docs for the options list (min, max, step, stars, displayOnly, showClear, showCaption, theme), then the theme CSS folders. The plugin replaces your <input> with a <div class="rating-container"> and keeps the original input in sync, so your form still submits normally.
Reusable Blade component. Wrap the markup + init so you don't repeat it:
{{-- resources/views/components/star-rating.blade.php --}}
@props(['name', 'value' => 0, 'readonly' => false, 'step' => 0.5])
<input type="number" id="{{ $name }}" name="{{ $name }}"
value="{{ old($name, $value) }}" step="{{ $step }}" min="0" max="5"
class="rating" @if($readonly) disabled @endif>
@once
@push('scripts')
<script>
document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.rating').forEach(el => {
const el$ = window.jQuery(el);
el$.rating({
step: parseFloat(el.step) || 0.5,
stars: 5, min: 0, max: 5,
showClear: !el.disabled,
showCaption: false,
displayOnly: el.disabled,
theme: 'krajee-fa',
});
});
});
</script>
@endpush
@endonce
Server-side validation is mandatory — the plugin gives you nothing on the backend:
$data = $request->validate([
'rating' => ['required', 'numeric', 'min:0.5', 'max:5', function ($attr, $value, $fail) {
if (fmod($value * 2, 1) !== 0.0) { // step 0.5
$fail('Rating must be in half-star increments.');
}
}],
]);
$review = $product->reviews()->create([
'rating' => $data['rating'],
'user_id' => auth()->id(),
]);
Store as decimal(3,2) and cast to float in the model so averages render cleanly.
Read-only average display (product page, listing cards) — build once, no form:
<input type="number" value="{{ round($product->reviews_avg_rating, 1) }}"
class="rating-display" step="0.1" min="0" max="5">
<script>
$('.rating-display').rating({
displayOnly: true, showClear: false, showCaption: true,
starCaptions: v => v.toFixed(1) + ' / 5',
theme: 'krajee-fa'
});
</script>
Instant/AJAX save. Listen for the plugin's event, not the native change:
$('#rating').on('rating:change', function (event, value, caption) {
if (!value) return; // clear button
fetch('/api/ratings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': document.querySelector('meta[name="csrf-token"]').content,
'Accept': 'application/json',
},
body: JSON.stringify({ product_id: productId, rating: value }),
}).then(r => r.ok ? r.json() : Promise.reject(r));
});
Programmatic updates. Never $('#rating').val(4).trigger('change') — the plugin won't redraw. Use:
$('#rating').rating('update', 4.5); // syncs stars + input
$('#rating').rating('reset'); // back to default
$('#rating').rating('refresh'); // re-read options/state
Livewire / Alpine. The plugin mutates the DOM, so morphing breaks it. Guard the container with wire:ignore, or re-init on livewire:navigated / Alpine x-init and call .rating('destroy') before re-rendering:
<div wire:ignore>
<input type="number" wire:model="rating" class="rating" step="0.5" min="0" max="5">
</div>
krajee-gly assume Bootstrap 3 glyphicons and will render empty boxes on modern stacks — use krajee-fa (Font Awesome) or krajee-uni (self-contained SVG/font).window.jQuery is set is the #1 "rating is not a function" cause in Vite builds.<input> gets hidden and replaced. $('#rating').val() may read stale data mid-interaction. Always go through the API ('update', 'reset') and read the value on the server, never from the DOM.rating:change is reliable. The plugin emits rating:change, rating:clear, rating:reset. Programmatic updates don't always fire native change. Bind to the plugin events.0. If the field is required, showClear: true lets users submit nothing. Either drop showClear on required fields or validate required and render a sensible message. Don't cast '' to 0 accidentally — that "no rating" becomes a one-star review in averages.step must divide max - min cleanly. step: 0.3 with max: 5 produces off-grid values and ugly captions. Use 0.5, 0.25, or 0.1. Fractional precision is the plugin's selling point — don't fight it with a nullable|integer rule.stars vs max. max is the numeric ceiling; stars is how many icons are drawn. Setting stars: 10, max: 5 gives you 10 half-stars — intentional, but confusing if you didn't plan for it.displayOnly: true ignores disabled/value changes unless you call 'refresh' or re-init. For listings that re-render via Turbo/Livewire, re-initialize explicitly.js/locales/star-rating.<lang>.js after the plugin, then pass language: 'es'. It won't auto-detect from Laravel's app()->getLocale() — pass it through a view composer or @js.aria-light divs. Pair them with a visible numeric label for screen readers and keep the real numeric input (even hidden) for form semantics.rating=99 or rating=-3. Enforce min/max/step on the Laravel side; the plugin is a UX layer only.@vite. If stars render blank, either point the font url() at node_modules via resolve.alias, or fall back to the CDN for the theme stylesheet.value attribute on init — pass old('rating') in Blade, and it will render correctly on reload without extra JS.How can I help you explore Laravel packages today?