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.

Source in the MFEs
administrator-fe AppFormFooter.vuerate-fe AppFormFooter.tsxrate-fe app-form-footer.cssreference-fe AppFormFooter.tsxUpdateUser.vue (usage)rate-fe Inputdata/create.tsx (usage)

AppFormFooter

Component

Sticky Save / Cancel footer pinned to the bottom of the scroll container.

Download .zip
Install tosrc/components/common/Requiresantd
  • TSXAppFormFooter.tsx
  • CSSapp-form-footer.css
  • MDREADME.md

Live 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.

Preview
Layout:
Select role
<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 extrais-end, with extrais-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

NameTypeDefaultDescription
childrenReactNodePrimary form actions (Save / Cancel), always anchored LEFT. In the Vue version this is the default <slot />.
extraReactNodeOptional 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

Last child inside Form > Card
Render the footer as the last child inside the Card body, inside the Form. That keeps 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/Tabs trap the sticky footer
In rate-fe, 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).
Match the negative margin to your card padding
The bleed margins must equal the module’s actual .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.
racar-fe skill asset diverges
The skill asset copy of the component renders 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.