Feedback Modal with Star Rating and Comment — Free HTML CSS JS Snippet

Feedback Modal with Star Rating and Comment · Modals · Plain HTML, CSS & JS · Live preview

CategoryModals

What's included

Features

Five-star rating built from role="radio" buttons inside a role="radiogroup"
Separate hover-preview state and committed-rating state that never overwrite each other
Comment field is genuinely optional — Submit only requires a star rating, not text
Live character counter computed directly from textarea.value.length, no drift possible
Counter turns red only in the final 20 characters as a real running-low warning
Thank-you message text adapts based on whether the rating was 4-5 stars or lower
Full form reset deferred until after the close transition finishes, avoiding a visible flash
Escape key, backdrop click, and a close button all dismiss the modal
Export as HTML file, React JSX, or React + Tailwind CSS
Mobile (375px), Tablet (768px), Desktop device preview buttons

About this UI Snippet

Star Rating Feedback Modal — Hover Preview, Optional Comment, Rating-Aware Confirmation

Screenshot of the Feedback Modal with Star Rating and Comment snippet rendered live

A feedback prompt that's just a comment box gets low response rates — typing takes effort. This modal leads with the lowest-effort input possible (tapping a star) and treats the comment field as genuinely optional, so a visitor can leave useful signal in one click while still being invited to add more.

Stars as buttons, not radio inputs

Each star is a real <button> with role="radio" inside a role="radiogroup" container — a semantic five-option single-choice control, but built without native <input type="radio"> elements so the star glyphs themselves can be styled and animated directly rather than styled via a hidden-input-plus-label workaround.

Hover preview vs. committed rating are two different states

paintStars(value) is called both from mouseenter (to preview what clicking would select) and from mouseleave (to restore whatever the actual currentRating currently is). This is why hovering star 4 lights up four stars temporarily, but moving the mouse away without clicking reverts back to whatever was last actually chosen — the hover state never overwrites currentRating itself, only the visual paint.

Comment is genuinely optional — the label says so and the logic enforces it

updateSubmitState() only checks currentRating === 0 — the comment textarea's content plays no role in whether Submit is enabled. The label text itself says "(optional)" and the code backs that claim up, rather than silently requiring text anyway (a common trust-eroding pattern in feedback forms that claim optionality but don't enforce it).

A character counter that's just arithmetic on the real value

MAX_LEN - comment.value.length recalculates on every input event — there's no separate tracked character count that could drift from the textarea's actual content, and the counter turns red only in the last 20 characters as a genuine "running low" warning rather than being red by default.

The thank-you message reads the rating, not just a generic "thanks"

Before revealing #rfmThanks, the submit handler checks currentRating >= 4 and picks between an appreciative message and a more empathetic "this goes straight to the product team" message for lower ratings — a small but meaningful difference from a single static thank-you that would read as tone-deaf after a 1-star rating.

Full reset happens after the transition, not before

After showing the thank-you state for 1.6 seconds, the modal closes, and only *then* — after another 250ms matching the CSS close transition — does the code reset the rating, comment, and counter back to their initial state. Resetting immediately on close would risk the visitor briefly seeing the form flash back to empty while the modal is still visually closing.

Customizing it

Swap the five hardcoded labels (Poor through Excellent) for wording that matches your product's tone, adjust MAX_LEN for a longer or shorter comment allowance, or wire the submitBtn click handler's current setTimeout-based demo flow to a real fetch() call that posts { rating: currentRating, comment: comment.value } to your feedback endpoint.

Step by step

How to Use

  1. 1
    Open the modalClick "Rate your experience" to open the feedback dialog.
  2. 2
    Hover over the starsA live preview lights up stars under your cursor before you click.
  3. 3
    Click a star to commit your ratingThe Submit button enables only once a rating is chosen — the comment stays optional.
  4. 4
    Optionally add a commentThe character counter updates live and turns red in the final 20 characters.
  5. 5
    Submit and see the thank-you stateThe message adapts based on whether the rating was 4-5 stars or lower.
  6. 6
    Wire up a real endpointReplace the submit handler's setTimeout demo flow with a fetch() call posting the rating and comment.

Real-world uses

Common Use Cases

Post-purchase and post-support feedback
Prompt for a quick rating right after a checkout or support interaction while the experience is fresh.
In-app NPS and satisfaction surveys
Pair with an NPS survey widget for a broader satisfaction-tracking program.
Feature and beta feedback collection
Trigger this modal after a visitor tries a new feature to capture reaction while it's top of mind.
Learn hover-vs-committed UI state
Study how paintStars() serves two different callers (hover preview and click commit) without either overwriting the other's intent.
App store and marketplace review prompts
Reuse the star-plus-optional-comment pattern before deep-linking a high rating out to a public review page.
Related: Helpful Feedback Widget
Pair with the Helpful Feedback Widget for a lighter-weight, inline alternative to this modal.

Got questions?

Frequently Asked Questions

Each star is a real <button> with role="radio" inside a role="radiogroup" wrapper, giving the same semantic single-choice meaning as native radio inputs while allowing the star glyph itself to be styled and scaled directly on hover and click, without the usual hidden-input-plus-styled-label workaround radio buttons typically need for custom styling.

paintStars(value) only updates which stars visually appear filled — it never touches the currentRating variable. Hovering calls paintStars() with the hovered star's value; moving the mouse away calls paintStars(currentRating), repainting back to whatever was last actually clicked. Only a click updates currentRating itself.

No. updateSubmitState() disables the Submit button based solely on whether currentRating is still 0 — the comment textarea's content is never checked. The label explicitly says "(optional)" and the enabling logic honors that rather than silently requiring text anyway.

It recalculates MAX_LEN - comment.value.length directly from the textarea's live value on every input event — there is no separately tracked counter variable that could fall out of sync with what is actually typed. It only switches to red text once 20 or fewer characters remain.

The submit handler checks whether currentRating is 4 or 5 and picks between an appreciative message and a more empathetic message acknowledging the feedback will reach the product team for lower ratings. A single generic "thanks!" message would feel tone-deaf immediately after someone leaves a 1-star rating.

The reset (clearing the rating, comment, and counter) is deferred by roughly 250ms after the close animation starts — matching the CSS transition duration — so the visitor never sees the form flash back to its empty starting state while the modal is still visibly animating closed.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Rather than guessing why hovering a star doesn't overwrite the rating you already picked, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how paintStars() is shared between the hover-preview and click-commit code paths without either one clobbering the other's intent, and why the character counter recalculates from the textarea's live value instead of tracking a separate counter variable. The same assistant can help you extend it — ask it to wire the submit handler to a real fetch() call posting { rating, comment } to your feedback API with proper error handling if the request fails, add a "why this rating?" set of quick-select reason chips that appear only for low ratings (1-2 stars) to gather more specific signal, or persist a "already submitted feedback today" flag in localStorage so the prompt doesn't reappear too often for the same visitor. It's also useful for an accessibility review: ask whether the star radiogroup should support arrow-key navigation between options in addition to click and hover, which is expected behavior for a native ARIA radiogroup pattern. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build a feedback modal in plain HTML, CSS, and vanilla JavaScript combining a five-star rating picker with an optional comment field, in a single reusable component — no library.

Requirements:
- A trigger button that opens a modal (backdrop + centered dialog with a fade/scale transition) containing five star buttons built as real <button> elements with role="radio" inside a role="radiogroup" container (not native radio inputs), plus a textarea for an optional comment with a maxlength and a live character-remaining counter.
- Hovering over a star must preview that rating by visually filling stars up to the hovered one, and moving the mouse away without clicking must revert the display back to whatever rating was actually last clicked — the hover preview and the committed rating must be tracked as two logically separate states so neither overwrites the other unintentionally.
- Clicking a star commits that rating, updates a small text label describing the rating in words (e.g. "Poor" through "Excellent"), and updates the corresponding aria-checked attributes on the star buttons.
- A Submit button must remain disabled until a star rating has been chosen, and must NOT require any text in the comment field — the comment must be genuinely optional both in its label text and in the actual enabling logic.
- The character counter must be computed directly from the textarea's current value length on every input event (not from a separately maintained counter variable) and switch to a warning color only once remaining characters drop to 20 or fewer.
- On submit, replace the form content with a thank-you confirmation whose message text differs depending on whether the rating was high (4-5 stars) versus lower, then automatically close the modal after a short delay, and only after the close animation would have finished, reset all the modal's internal state (rating, comment text, counter) back to its initial values so the next time it opens it starts fresh.
- Support closing the modal via a close button, backdrop click, and the Escape key at any point.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Source Code

<div class="rfm-page"><button type="button" class="rfm-open" id="rfmOpen">Rate your experience</button></div>

<div class="rfm-backdrop" id="rfmBackdrop"></div>
<div class="rfm-modal" id="rfmModal" role="dialog" aria-modal="true" aria-labelledby="rfmTitle">
  <button type="button" class="rfm-close" id="rfmClose" aria-label="Close">✕</button>

  <div class="rfm-body" id="rfmBody">
    <h3 id="rfmTitle">How was your experience?</h3>
    <p class="rfm-sub">Your feedback helps us improve. This takes less than a minute.</p>

    <div class="rfm-stars" id="rfmStars" role="radiogroup" aria-label="Star rating">
      <button type="button" class="rfm-star" data-value="1" role="radio" aria-checked="false" aria-label="1 star">★</button>
      <button type="button" class="rfm-star" data-value="2" role="radio" aria-checked="false" aria-label="2 stars">★</button>
      <button type="button" class="rfm-star" data-value="3" role="radio" aria-checked="false" aria-label="3 stars">★</button>
      <button type="button" class="rfm-star" data-value="4" role="radio" aria-checked="false" aria-label="4 stars">★</button>
      <button type="button" class="rfm-star" data-value="5" role="radio" aria-checked="false" aria-label="5 stars">★</button>
    </div>
    <p class="rfm-rating-label" id="rfmRatingLabel">&nbsp;</p>

    <label class="rfm-comment-label" for="rfmComment">Tell us more <span>(optional)</span></label>
    <textarea id="rfmComment" class="rfm-comment" rows="3" maxlength="280" placeholder="What worked well? What could be better?"></textarea>
    <span class="rfm-counter" id="rfmCounter">280 left</span>

    <button type="button" class="rfm-submit" id="rfmSubmit" disabled>Submit feedback</button>
  </div>

  <div class="rfm-thanks" id="rfmThanks" hidden>
    <div class="rfm-check">
      <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6L9 17l-5-5"/></svg>
    </div>
    <h3>Thanks for the feedback!</h3>
    <p id="rfmThanksSub">We appreciate you taking the time.</p>
  </div>
</div>