Results Overlay
Reports what a batch operation actually did, as an itemised list the user can read before moving on.
When to Use
Variants
Overlay
Covers its positioned parent — typically a modal body once the operation finishes. It needs a position: relative ancestor or it will cover the wrong thing.
This content sits behind the overlay.
<?= ws_results_overlay([
'id' => 'cleanupResults',
'variant' => 'overlay',
'title' => 'Cleanup complete',
'icon' => 'check_circle',
'buttonText' => 'Done',
'buttonId' => 'cleanupDoneBtn',
]) ?>Inline
Sits in the flow below the content it reports on, with no button and no overlay positioning. Use it when the user stays on the page afterwards.
- Rounded 3 activity durations
- Added 1 break after 90 minutes
- Fixed 2 title formatting issues
<?= ws_results_overlay([
'id' => 'inlineResults',
'variant' => 'inline',
'title' => 'Cleanup complete',
]) ?>States
| State | Behavior |
|---|---|
| Hidden | The initial state. The markup renders on page load but stays hidden until you reveal it. |
| Visible | You add .visible and set the display. The component ships no JS — there is no wsResultsShow(); revealing and hiding are yours. |
| Populated | The list renders empty. Fill #{id}-list with <li> items from your operation's real result — the component has no knowledge of what happened. |
| Empty result | An operation that changed nothing produces an empty list under a "complete" heading, which reads as a bug. Branch on the result count and show a different message. |
| Button | Overlay variant only. Rendered with your buttonId so you can bind to it — nothing is bound for you. |
Real-World Usage
The Planner's agenda cleanup modal. The overlay is placed inside the modal body, populated from the API response, and revealed when the operation finishes — with the announcement and focus handling the component doesn't provide.
<?php ws_modal_start(['id' => 'cleanup', 'title' => 'Clean up agenda']); ?>
<div style="position: relative;">
<?= ws_checkbox_card_group($operations, ['label' => 'What should we clean up?']) ?>
<?= ws_results_overlay([
'id' => 'cleanupResults',
'title' => 'Cleanup complete',
'buttonText' => 'Done',
'buttonId' => 'cleanupDone',
]) ?>
</div>
<?php ws_modal_end(); ?>
<script>
async function runCleanup(planId, ops) {
const res = await fetch('/api/plan-items.php?action=cleanup', { /* … */ });
const data = await res.json();
const overlay = document.getElementById('cleanupResults');
const list = document.getElementById('cleanupResults-list');
if (!data.changes.length) { // nothing changed — don't claim success
wsToastInfo('Nothing needed cleaning up');
return;
}
list.innerHTML = data.changes.map(c => `<li>${escapeHtml(c)}</li>`).join('');
overlay.classList.add('visible');
overlay.style.display = 'flex';
// The component announces nothing and moves no focus — do both.
overlay.setAttribute('role', 'status');
document.getElementById('cleanupDone').focus();
}
document.getElementById('cleanupDone').addEventListener('click', () => {
wsModalClose('cleanup');
location.reload();
});
</script>Options
| Option | Type | Default | Purpose |
|---|---|---|---|
id | string | '' | Required in practice — every other ID derives from it, and it's how you reveal the panel. |
variant | string | 'overlay' | overlay | inline |
title | string | 'Complete' | Heading. Name the operation: "Cleanup complete". |
icon | string | 'check_circle' | Material icon beside the title |
buttonText | string | 'Done' | Action button label. Overlay variant only. |
buttonId | string | '{id}-done' | Button ID for binding. Overlay variant only. |
listId | string | '{id}-list' | The <ul> you populate |
class | string | '' | Additional CSS classes |
Accessibility
| Concern | Behavior |
|---|---|
| Structure | Results are a real <ul>, so once populated the count and items are announced properly. |
| Keyboard | The overlay's action button is a real button. The inline variant has nothing focusable. |
| Announcement | Missing. No role="status" or aria-live is applied, so revealing the overlay is silent to screen-reader users — the operation appears to have done nothing. Set role="status" on the container when you reveal it, as in the example above. |
| Focus | Missing. Focus isn't moved when the overlay appears, so a keyboard user stays wherever they were — behind a panel they can't see. Move focus to the Done button on reveal. |
| Covering content | The overlay hides its parent visually but doesn't hide it from assistive tech — no aria-hidden or inert is applied underneath. A screen reader can still reach the covered controls. |
| Icon | Decorative. The title carries the outcome, so don't rely on the tick to convey success. |
Tokens
The panel draws on the shared surface, success, radius, and type scales rather than defining its own token set — deliberately, so a results panel matches whatever surface it covers. See Token Reference for current values and the .ws-results rules in components.css for the exact declarations.
CSS Classes
| Class | Purpose |
|---|---|
.ws-results | Base panel, hidden until revealed |
.ws-results--inline | Inline variant — no overlay positioning |
.ws-results.visible | Revealed state. Added by your code, not the component. |
.ws-results__header | Icon and title row |
.ws-results__icon | Outcome icon |
.ws-results__title | Heading text |
.ws-results__list | The <ul> you populate, id="{id}-list" |
The class prefix is .ws-results, not .ws-results-overlay — helper and class names differ here, as with the planner rails.
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_results_overlay() helper function |
includes/components/results-overlay.php | Template — header, empty list, and optional button |
includes/components/components.css | Styles (.ws-results rules) |
No JS file — reveal, populate, announce, and focus are all consumer-side.