Report Content Modal with Reason Picker — Free HTML CSS JS Snippet
Report Content Modal with Reason Picker · Modals · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Report Content Modal — Conditional Details Field and a Confirmation Step

A generic "report this" button that just says "thanks" gives a moderation team nothing to act on. This modal collects a specific, structured reason via radio buttons, reveals an optional free-text field only when the selected reason genuinely benefits from more context, and swaps in a real confirmation step — including which reason was reported — once submitted.
One `updateFormState()` function governs visibility, hint text, and the submit button together
Every interaction — selecting a reason or typing into the details textarea — calls updateFormState(), which reads the currently checked radio via getSelectedReason() and then decides three things at once: whether the details textarea is shown, what the hint line beneath the reasons says, and whether the submit button is enabled. Because all three come from the same function call reading the same current state, there's no path where the hint text could say "choose a reason" while the submit button is somehow already enabled.
The details field's visibility is reason-dependent, not a blanket option
detailsWrap.hidden = reason !== 'Something else' means the optional context field only appears for the one reason that's genuinely ambiguous without it — a report for "Hate speech" or "Violent or graphic content" doesn't need an extra prompt, since the category itself is the useful signal. This keeps the modal shorter for the common cases while still gathering more context exactly where it's most valuable.
"Something else" has its own validation branch
Selecting "Something else" without typing anything into the details field keeps the submit button disabled and shows a specific hint ("Please add a few details...") rather than silently accepting an unhelpful, contentless report. Every other reason only requires the radio selection itself — the validation branches based on which reason is selected, not one uniform rule applied to all five.
A radio group built from a native `<input type="radio">`, styled but not reinvented
Each .rcm-reason label wraps a visually-hidden native radio input plus a custom .rcm-radio circle styled via the sibling combinator (input:checked ~ .rcm-radio) and a :has() selector highlighting the whole row. Using real radio inputs (rather than a fully custom JS-driven single-select) means keyboard navigation, form semantics, and "exactly one can be checked" are all handled by the browser's native radio-group behavior for free.
Submission swaps the whole step, it doesn't just reset the form silently
Clicking submit hides #rcmStepReason and reveals #rcmStepDone — including a confirmation sentence that names the specific reason that was reported ("Thanks for flagging this as 'Harassment or bullying'") — rather than just clearing the radio buttons and showing a generic toast. The form only actually resets when the visitor closes the confirmation step via doneBtn, delayed until after the modal's own closing transition finishes.
Customizing it
Wire the commented-out fetch() call at the point where reason and details are collected to a real moderation endpoint. Add a sixth reason by adding another .rcm-reason label with a new value — reasonInputs is queried generically from input[name="rcmReason"], so getSelectedReason() and updateFormState() require no changes for it to work correctly.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Rather than tracing the conditional logic by hand, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how updateFormState() ties the details field's visibility, the hint text, and the submit button's enabled state together to the currently selected reason, and why "Something else" gets its own validation branch requiring non-empty details text while the other four reasons do not. The same assistant can help you extend it — ask it to wire the commented-out fetch() call to a real moderation backend and handle a failed submission by showing an error state instead of jumping straight to the confirmation step, add a secondary "block this user" checkbox that appears only for the Harassment or Hate speech reasons, or rate-limit the report button client-side so a visitor cannot submit multiple reports on the same content in quick succession. It's also useful for an accessibility review: ask whether the confirmation step should receive programmatic focus when it appears, and whether the radio group needs a fieldset and legend for correct screen-reader grouping semantics. 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:
Build a content-report modal in plain HTML, CSS, and vanilla JavaScript with a radio-button reason picker, a conditionally shown optional details field, and a real confirmation step — no framework.
Requirements:
- A trigger button that opens a modal (backdrop plus centered dialog with a fade/scale transition), closable via a close button, backdrop click, and the Escape key.
- Inside the modal, five reason options as a native radio-button group (same name attribute, one value each), each showing a bold label and a short description, with a custom-styled radio indicator (the native input visually hidden, a styled circle indicator driven by its checked state) and the entire row highlighted when its radio is selected.
- Below the reasons, an optional textarea with a character counter and a real maxlength attribute, hidden by default — it must only become visible when the specific reason meant for open-ended cases (e.g. "Something else") is selected, and stay hidden for every other reason.
- Write one function that runs on every reason change and every textarea input, deciding together: whether the details field is shown, what a hint message beneath the reasons says, and whether the submit button is enabled. The submit button must specifically stay disabled if no reason is selected, and separately stay disabled if the details-requiring reason is selected but the textarea is empty or whitespace-only — every other reason should enable the button as soon as it is selected, with no details required.
- Clicking submit must not simply reset the form or show a toast — it must hide the reason-picking content and reveal a distinct confirmation view inside the same modal, with a message that names the specific reason that was reported.
- The confirmation view's own close button must close the modal and only then (after a short delay so it happens after the modal's closing transition) reset every field — unchecking the radios, clearing the textarea and its character count, and switching back to the reason-picking view — so reopening the modal always starts fresh.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.
Step by step
How to Use
- 1Open the report modalClick "Report this post" to open the reason picker.
- 2Select a reasonThe submit button enables once a reason is chosen — except for "Something else."
- 3Try "Something else"A details textarea appears and is required before the submit button enables.
- 4Submit the reportThe reason picker is replaced by a confirmation step naming the reason you selected.
- 5Close the confirmationThe form resets fully so reopening the modal starts fresh.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component, or "Tailwind" for a React + Tailwind version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
detailsWrap.hidden is set to reason !== 'Something else' inside updateFormState(), so the field only becomes visible when that specific radio is selected. The other four reasons are specific enough categories that an extra free-text prompt is not needed to make the report actionable, which keeps the modal shorter for the common cases.
No — updateFormState() has a dedicated check for reason === 'Something else' && detailsTextarea.value.trim() === '', which keeps the submit button disabled and shows a hint asking for details specifically in that case. Every other reason only requires the radio selection itself to enable the button.
The reason and any details text are collected (shown at the point where a real fetch() call to a backend endpoint would send them), and the modal swaps from the reason-picker step to a confirmation step that names the specific reason you selected in its message, rather than just clearing the form and showing a generic success message.
No — the reason-picker step is hidden and the confirmation step is shown, but the actual form fields (radio selection, textarea content) are not cleared until the visitor clicks the confirmation step's Close button, and even then the reset is delayed slightly to let the modal's own closing transition finish first.
All five reason options are native input type="radio" elements sharing the same name="rcmReason" attribute, so the browser's built-in radio-group behavior guarantees only one can be checked at a time — no custom JavaScript single-select logic is needed for that part.
Add another .rcm-reason label following the same markup pattern, with its own radio input sharing name="rcmReason" and a distinct value attribute. reasonInputs is queried generically via document.querySelectorAll('input[name="rcmReason"]'), so getSelectedReason() and updateFormState() both pick up the new option automatically with no other JavaScript changes.