Canvas Design System
Main Site Tokens

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

Use when: Any Modal needs actions. It replaced the hand-written footers across 30-plus modals, which is precisely the value — cancel and confirm land in the same place every time.
Don't use when: The actions aren't in a modal. For a form's own submit row, lay out Button directly — this component's border and padding assume a dialog edge.

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

StateBehavior
DefaultA top border separating the footer from the modal body, with the buttons right-aligned in .ws-modal-footer__right.
With a left actionThe 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 statesHover, focus, disabled, and loading all belong to Button. Pass loading or disabled through the relevant slot's array.
Omitted slotsEvery 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.

OptionTypeDefaultPurpose
cancelarraynullDismiss button, rendered as a ghost. Accepts text, id, class, attrs.
primaryarraynullConfirm button, rightmost. Accepts text, id, icon, class, attrs, fullWidth.
leftarraynullFar-left secondary action. Accepts text, id, icon, variant, class, attrs.
idstringnullID on the footer element
classstring''Additional CSS classes on the footer
info Only 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

ConcernBehavior
SemanticsA plain container of real <button> elements. No ARIA is added, and none is needed — the surrounding role="dialog" provides the context.
KeyboardNative. Tab order follows the DOM: left action, then cancel, then primary — which matches the visual left-to-right reading.
FocusEach button keeps its native focus ring. The modal's focus trap keeps Tab cycling within the dialog.
Button labelsName 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 actionsColour alone can't mark danger. Put the verb in the label, as in "Delete workshop" — the red is reinforcement.
Cancel and EscBoth 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

TokenUsed for
--border-defaultTop border separating footer from body
--space-2 / --space-3Gap 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

ClassPurpose
.ws-modal-footerFooter row with its top border
.ws-modal-footer--singleSingle full-width button layout
.ws-modal-footer__leftLeft action group
.ws-modal-footer__rightCancel and primary group
info Modal also defines .ws-modal__footer with --between and --start modifiers — a separate, older set of footer styles. Prefer this component so the ordering stays consistent.

Files

FilePurpose
includes/components/helpers.phpws_modal_footer() helper function
includes/components/modal-footer.phpTemplate — slot layout and the button calls
includes/components/components.cssStyles (.ws-modal-footer rules)