Modal Footer
Fixes the button order in a modal's action row, so every dialog on the platform resolves the same way.
When to Use
Variants
Cancel and confirm
The default. Cancel is a ghost button, the primary sits to its right — the rightmost position being the one people reach for.
<?= ws_modal_footer([
'cancel' => ['text' => 'Cancel', 'id' => 'cancelBtn'],
'primary' => ['text' => 'Export Now', 'id' => 'exportBtn'],
]) ?>With a left action
A third button, pushed to the far left, for a secondary path that isn't cancelling — downloading, learning more, switching method. Keep it visually quieter than the primary.
<?= ws_modal_footer([
'left' => ['text' => 'Download .ics', 'icon' => 'download', 'variant' => 'ghost'],
'cancel' => ['text' => 'Cancel'],
'primary' => ['text' => 'Send Invite', 'icon' => 'send'],
]) ?>Single full-width action
When there's nothing to cancel — an acknowledgement, or a modal whose only exit is the close button. Adds .ws-modal-footer--single.
<?= ws_modal_footer([
'primary' => ['text' => 'Add Activity', 'fullWidth' => true],
]) ?>States
| State | Behavior |
|---|---|
| Default | A top border separating the footer from the modal body, with the buttons right-aligned in .ws-modal-footer__right. |
| With a left action | The left button occupies .ws-modal-footer__left and the row becomes space-between. |
| Single | .ws-modal-footer--single adjusts the layout so one full-width button fills the row. |
| Button states | Hover, focus, disabled, and loading all belong to Button. Pass loading or disabled through the relevant slot's array. |
| Omitted slots | Every slot is optional. Passing none renders an empty bordered strip — skip the call instead. |
Real-World Usage
A destructive confirmation. The primary slot takes class, so the confirm button becomes the danger variant while keeping its position — muscle memory intact, colour signalling the consequence.
<?php ws_modal_start(['id' => 'confirm-delete', 'title' => 'Delete this workshop?', 'size' => 'sm']); ?>
<p>This cannot be undone.</p>
<?= ws_modal_footer([
'cancel' => ['text' => 'Keep it',
'attrs' => ['onclick' => "wsModalClose('confirm-delete')"]],
'primary' => ['text' => 'Delete workshop',
'icon' => 'delete',
'class' => 'ws-btn--danger',
'attrs' => ['onclick' => 'deleteWorkshop(42)']],
]) ?>
<?php ws_modal_end(); ?>Options
Three button slots plus wrapper options. Each slot takes an array that's forwarded to ws_button(), so anything Button accepts works there.
| Option | Type | Default | Purpose |
|---|---|---|---|
cancel | array | null | Dismiss button, rendered as a ghost. Accepts text, id, class, attrs. |
primary | array | null | Confirm button, rightmost. Accepts text, id, icon, class, attrs, fullWidth. |
left | array | null | Far-left secondary action. Accepts text, id, icon, variant, class, attrs. |
id | string | null | ID on the footer element |
class | string | '' | Additional CSS classes on the footer |
left exposes a variant. Cancel is always ghost and primary is always primary — that's the consistency the component exists to enforce. To change the primary's appearance, pass a modifier through class as in the danger example above.
Accessibility
| Concern | Behavior |
|---|---|
| Semantics | A plain container of real <button> elements. No ARIA is added, and none is needed — the surrounding role="dialog" provides the context. |
| Keyboard | Native. Tab order follows the DOM: left action, then cancel, then primary — which matches the visual left-to-right reading. |
| Focus | Each button keeps its native focus ring. The modal's focus trap keeps Tab cycling within the dialog. |
| Button labels | Name the action, not the answer: "Delete workshop" beats "OK". A screen-reader user reaching the footer out of context should still know what the button does. |
| Destructive actions | Colour alone can't mark danger. Put the verb in the label, as in "Delete workshop" — the red is reinforcement. |
| Cancel and Esc | Both should do the same thing. If your cancel button runs cleanup, make sure the modal's Esc handler runs it too — Esc bypasses the button entirely. |
Tokens
| Token | Used for |
|---|---|
--border-default | Top border separating footer from body |
--space-2 / --space-3 | Gap between buttons and the footer's internal padding |
The footer is deliberately thin on tokens — it's a layout shell. All colour and type come from the buttons inside it.
CSS Classes
| Class | Purpose |
|---|---|
.ws-modal-footer | Footer row with its top border |
.ws-modal-footer--single | Single full-width button layout |
.ws-modal-footer__left | Left action group |
.ws-modal-footer__right | Cancel and primary group |
.ws-modal__footer with --between and --start modifiers — a separate, older set of footer styles. Prefer this component so the ordering stays consistent.
Files
| File | Purpose |
|---|---|
includes/components/helpers.php | ws_modal_footer() helper function |
includes/components/modal-footer.php | Template — slot layout and the button calls |
includes/components/components.css | Styles (.ws-modal-footer rules) |