Patterns
Form Footer
AppFormFooter — the sticky Save/Cancel bar pinned to the bottom of every create/edit form, with primary actions on the left and optional extra actions pushed right.
AppFormFooter
ComponentSticky Save / Cancel footer pinned to the bottom of the scroll container.
src/components/common/RequiresantdLive example
The sticky action bar at the bottom of every create/edit form: position: sticky; bottom: 0 keeps Save/Cancel visible while the form scrolls. Two layouts exist — is-end (primary actions anchored left, nothing else) and is-between (an extra action pushed to the right). Toggle both below, then scroll the card.
<Form layout="vertical" onFinish={handleSubmit}>
<Row gutter={[12, 0]}>
<Col xs={24} md={12}>
<Form.Item name="fullName" label="Full Name:" rules={[{ required: true }]}>
<Input placeholder="Enter full name" />
</Form.Item>
</Col>
</Row>
{/* Sticky footer - LAST child inside Form > Card */}
<AppFormFooter
extra={
<Button onClick={handleChangePassword}>
Change Password
</Button>
}
>
<Button type="primary" htmlType="submit" loading={isLoading}>
Save
</Button>
<Button onClick={handleCancel}>
Cancel
</Button>
</AppFormFooter>
</Form>Component
The component is a thin wrapper that picks the modifier class from whether extra is present: no extra → is-end, with extra → is-between. It ships identically in every React MFE and as a slot-based Vue equivalent in administrator-fe.
// React variant (rate-fe, reference-fe, report-fe, racar-fe)
import type { ReactNode } from 'react';
import './app-form-footer.css';
type AppFormFooterProps = {
children: ReactNode;
extra?: ReactNode;
};
const AppFormFooter = ({ children, extra }: AppFormFooterProps) => (
<div className="app-form-footer">
<div className={`app-form-footer__inner ${extra ? 'is-between' : 'is-end'}`}>
<div className="app-form-footer__actions">{children}</div>
{extra && <div className="app-form-footer__extra">{extra}</div>}
</div>
</div>
);
export default AppFormFooter;Styles
The whole trick is in the CSS: the footer sticks to bottom: 0 of the nearest scroll container (typically .layout-content), and its negative margins bleed to the card edges — -12px margins cancel the module’s 12px .ant-card-body padding so the bar spans the full card width. The opaque #ffffff background masks form content scrolling underneath.
.app-form-footer {
position: sticky;
bottom: 0;
z-index: 10;
margin: 16px -12px -12px -12px; /* Cancels 12px card padding */
padding: 12px;
background: #ffffff;
border-top: 1px solid #f0f0f0;
border-radius: 0 0 8px 8px;
}
.app-form-footer__inner {
display: flex;
align-items: center;
gap: 12px;
row-gap: 8px;
flex-wrap: wrap;
}
.app-form-footer__inner.is-end {
justify-content: flex-start; /* Primary actions LEFT */
}
.app-form-footer__inner.is-between {
justify-content: space-between; /* Primary LEFT, extra RIGHT */
}
.app-form-footer__extra,
.app-form-footer__actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
/* For rate-fe Collapse/Tabs overflow:hidden trapping, neutralize to escape sticky */
.app-sticky-footer-host,
.app-sticky-footer-host .ant-collapse-item,
.app-sticky-footer-host .ant-collapse-content,
.app-sticky-footer-host .ant-collapse-content-box,
.app-sticky-footer-host .ant-tabs,
.app-sticky-footer-host .ant-tabs-content-holder,
.app-sticky-footer-host .ant-tabs-content,
.app-sticky-footer-host .ant-tabs-tabpane {
overflow: visible !important;
}Props
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Primary form actions (Save / Cancel), always anchored LEFT. In the Vue version this is the default <slot />. |
extra | ReactNode | — | Optional secondary actions (e.g. Change Password), pushed to the RIGHT. Its presence switches the inner layout from is-end to is-between. Vue: named slot "extra". |
Rules
htmlType="submit" buttons within the Form context so AntD validation and onFinish / @finish keep firing, and keeps the sticky positioning inside the card’s scroll container. Never mount it outside the Form or Card.Collapse and Tabs use overflow: hidden, which traps position: sticky. Add the app-sticky-footer-host class to the Collapse root — it neutralizes overflow on the Collapse/Tabs wrappers (see the last block of the SCSS above) so the footer escapes up to .layout-content. Not needed in administrator-fe / reference-fe (plain Card, visible overflow)..ant-card-body padding: administrator-fe overrides it to 12px !important and reference-fe/rate-fe use -12px margins with 12px padding, while racar-fe uses -16px margins with 12px 16px padding. Check your module’s card padding before copying the CSS.extra before the actions and gives is-end a flex-end justify — actions on the RIGHT. The real component at racar-fe/src/components/common/AppFormFooter.tsx matches administrator-fe (actions LEFT); copy from the real component, not the asset.