diff --git a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap
index 01fbd65f7e..b24be0c032 100644
--- a/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap
+++ b/src/__tests__/snapshot-tests/__snapshots__/documenter.test.ts.snap
@@ -17520,6 +17520,28 @@ render to an element under \`document.body\`.",
"optional": true,
"type": "HTMLElement",
},
+ {
+ "defaultValue": "'center'",
+ "description": "Controls the vertical positioning of the modal.
+
+- \`center\` (default) - Modal is vertically centered in viewport and re-centers
+ when content height changes. Use for dialogs with static, predictable content.
+
+- \`top\` - Modal anchors at fixed distance and grows downward
+ as content expands. Use when content changes dynamically to prevent disruptive
+ vertical repositioning that causes users to lose focus.",
+ "inlineType": {
+ "name": "ModalProps.Position",
+ "type": "union",
+ "values": [
+ "center",
+ "top",
+ ],
+ },
+ "name": "position",
+ "optional": true,
+ "type": "string",
+ },
{
"description": "Use this property when \`getModalRoot\` is used to clean up the modal root
element after a user closes the dialog. The function receives the return value
@@ -17542,8 +17564,8 @@ of the most recent getModalRoot call as an argument.",
{
"defaultValue": "'medium'",
"description": "Sets the width of the modal. \`max\` uses variable width up to the
-largest size allowed by the design guidelines. Other sizes
-(\`small\`/\`medium\`/\`large\`) have fixed widths.",
+largest size allowed by the design guidelines. Other sizes:
+\`small\` (320px), \`medium\` (600px), \`large\` (820px), \`x-large\` (1024px), \`xx-large\` (1280px).",
"inlineType": {
"name": "ModalProps.Size",
"type": "union",
@@ -17552,6 +17574,8 @@ largest size allowed by the design guidelines. Other sizes
"max",
"medium",
"large",
+ "x-large",
+ "xx-large",
],
},
"name": "size",
diff --git a/src/modal/__tests__/modal.test.tsx b/src/modal/__tests__/modal.test.tsx
index 94d43364e1..a185397d0c 100644
--- a/src/modal/__tests__/modal.test.tsx
+++ b/src/modal/__tests__/modal.test.tsx
@@ -112,13 +112,22 @@ describe('Modal component', () => {
describe('size property', () => {
it('displays correct size', () => {
- (['small', 'medium', 'large', 'max'] as ModalProps.Size[]).forEach(size => {
+ (['small', 'medium', 'large', 'x-large', 'xx-large', 'max'] as ModalProps.Size[]).forEach(size => {
const wrapper = renderModal({ size });
expect(wrapper.findDialog().getElement()).toHaveClass(styles[size]);
});
});
});
+ describe('position property', () => {
+ it('displays correct position', () => {
+ (['center', 'top'] as ModalProps.Position[]).forEach(position => {
+ const wrapper = renderModal({ position });
+ expect(wrapper.findFocusLock().getElement()).toHaveClass(styles[`position-${position}`]);
+ });
+ });
+ });
+
describe('dismiss on click', () => {
it('closes the dialog when clicked on the overlay section of the container', () => {
const onDismissSpy = jest.fn();
diff --git a/src/modal/index.tsx b/src/modal/index.tsx
index c2b6982958..4195ce68c8 100644
--- a/src/modal/index.tsx
+++ b/src/modal/index.tsx
@@ -70,7 +70,7 @@ function ModalWithAnalyticsFunnel({
);
}
-export default function Modal({ size = 'medium', ...props }: ModalProps) {
+export default function Modal({ size = 'medium', position = 'center', ...props }: ModalProps) {
const { isInFunnel } = useFunnel();
const analyticsMetadata = getAnalyticsMetadataProps(props as BasePropsWithAnalyticsMetadata);
const baseComponentProps = useBaseComponent(
@@ -78,6 +78,7 @@ export default function Modal({ size = 'medium', ...props }: ModalProps) {
{
props: {
size,
+ position,
disableContentPaddings: props.disableContentPaddings,
flowType: analyticsMetadata.flowType,
},
@@ -95,12 +96,21 @@ export default function Modal({ size = 'medium', ...props }: ModalProps) {
analyticsMetadata={analyticsMetadata}
baseComponentProps={baseComponentProps}
size={size}
+ position={position}
{...props}
/>
);
}
- return ;
+ return (
+
+ );
}
applyDisplayName(Modal, 'Modal');
diff --git a/src/modal/interfaces.ts b/src/modal/interfaces.ts
index f13a4b90c3..99ff019aaf 100644
--- a/src/modal/interfaces.ts
+++ b/src/modal/interfaces.ts
@@ -26,10 +26,21 @@ export interface BaseModalProps {
export interface ModalProps extends BaseComponentProps, BaseModalProps {
/**
* Sets the width of the modal. `max` uses variable width up to the
- * largest size allowed by the design guidelines. Other sizes
- * (`small`/`medium`/`large`) have fixed widths.
+ * largest size allowed by the design guidelines. Other sizes:
+ * `small` (320px), `medium` (600px), `large` (820px), `x-large` (1024px), `xx-large` (1280px).
*/
size?: ModalProps.Size;
+ /**
+ * Controls the vertical positioning of the modal.
+ *
+ * - `center` (default) - Modal is vertically centered in viewport and re-centers
+ * when content height changes. Use for dialogs with static, predictable content.
+ *
+ * - `top` - Modal anchors at fixed distance and grows downward
+ * as content expands. Use when content changes dynamically to prevent disruptive
+ * vertical repositioning that causes users to lose focus.
+ */
+ position?: ModalProps.Position;
/**
* Determines whether the modal is displayed on the screen. Modals are hidden by default.
* Set this property to `true` to show them.
@@ -81,7 +92,8 @@ export interface ModalProps extends BaseComponentProps, BaseModalProps {
}
export namespace ModalProps {
- export type Size = 'small' | 'medium' | 'large' | 'max';
+ export type Size = 'small' | 'medium' | 'large' | 'x-large' | 'xx-large' | 'max';
+ export type Position = 'center' | 'top';
export interface DismissDetail {
reason: string;
diff --git a/src/modal/internal.tsx b/src/modal/internal.tsx
index 79e9e88c31..be6b41fa89 100644
--- a/src/modal/internal.tsx
+++ b/src/modal/internal.tsx
@@ -94,6 +94,7 @@ function PortaledModal({
children,
footer,
disableContentPaddings,
+ position = 'center',
onButtonClick = () => {},
onDismiss,
__internalRootRef,
@@ -108,7 +109,7 @@ function PortaledModal({
const instanceUniqueId = useUniqueId();
const headerId = `${rest.id || instanceUniqueId}-header`;
const lastMouseDownElementRef = useRef(null);
- const [breakpoint, breakpointsRef] = useContainerBreakpoints(['xs']);
+ const [breakpoint, breakpointsRef] = useContainerBreakpoints(['l']);
const i18n = useInternalI18n('modal');
const closeAriaLabel = i18n('closeAriaLabel', rest.closeAriaLabel);
@@ -247,7 +248,12 @@ function PortaledModal({
style={footerHeight ? { scrollPaddingBottom: footerHeight } : undefined}
data-awsui-referrer-id={subStepRef.current?.id || referrerId}
>
-
+