Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions packages/@godaddy/antares/components/modal/README.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
---
title: Modal
description: The Modal component provides a contextual dialog window that appears after explicit action and temporarily interrupts the user to demand input or present additional information.
---

## Features

- Three sizes: default (788px), large (1064px), and fullscreen
- Dismissable via close button, overlay click, or Escape key
- Optional media slot with inset or full-bleed display
- Horizontal layout with configurable media position (start/end)
- Fixed footer actions for scrollable content
- Accessible dialog with focus trapping and ARIA attributes
- Responsive: collapses to fullscreen on mobile
- Entry/exit animations

## Installation

```bash
npm install @godaddy/antares
```

## Examples

### Basic modal

import DefaultCode from './examples/default?raw';

<CodeBlock code={DefaultCode} />

### Modal with actions

import WithActionsCode from './examples/with-actions?raw';

<CodeBlock code={WithActionsCode} />

### Modal with media

import WithMediaCode from './examples/with-media?raw';

<CodeBlock code={WithMediaCode} />

### Horizontal media layout

import HorizontalMediaCode from './examples/horizontal-media?raw';

<CodeBlock code={HorizontalMediaCode} />

### Fullscreen

import FullscreenCode from './examples/fullscreen?raw';

<CodeBlock code={FullscreenCode} />

## Accessibility

### Keyboard interaction

| Key | Action |
|-----|--------|
| `Tab` | Moves focus to the next focusable element inside the modal |
| `Shift + Tab` | Moves focus to the previous focusable element inside the modal |
| `Escape` | Closes the modal (when dismissable) |

### ARIA

- `role="dialog"` is applied by default
- Use `role="alertdialog"` for confirmation dialogs that require user acknowledgment
- `ModalHeading` is linked via `aria-labelledby` automatically
- Focus is trapped within the modal while open
- Focus returns to the trigger element when the modal closes

## Best Practices

- Always include a `ModalHeading` for accessible labeling
- Use `isDismissable` for informational dialogs; omit for required actions
- Prefer `role="alertdialog"` when the user must acknowledge the content
- Use `fixedActions` on `ModalFooter` when modal content may scroll
- In horizontal layouts, ensure media has meaningful alt text or is decorative (`alt=""`)
13 changes: 13 additions & 0 deletions packages/@godaddy/antares/components/modal/examples/default.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { ModalTrigger, Modal, ModalHeading, Button } from '@godaddy/antares';

export function DefaultExample() {
return (
<ModalTrigger>
<Button variant="primary">Open modal</Button>
<Modal isDismissable>
<ModalHeading>Modal title</ModalHeading>
<p>This is the modal content. It provides a contextual dialog that temporarily interrupts the user.</p>
</Modal>
</ModalTrigger>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ModalTrigger, Modal, ModalHeading, ModalFooter, Button } from '@godaddy/antares';

export function FullscreenExample() {
return (
<ModalTrigger>
<Button variant="primary">Open fullscreen</Button>
<Modal size="fullscreen" isDismissable>
{({ close }) => (
<>
<ModalHeading>Fullscreen modal</ModalHeading>
<p>This modal takes up the entire viewport. Useful for complex workflows or immersive content.</p>
<ModalFooter>
<Button variant="secondary" onPress={close}>Cancel</Button>
<Button variant="primary" onPress={close}>Done</Button>
</ModalFooter>
</>
)}
</Modal>
</ModalTrigger>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { ModalTrigger, Modal, ModalHeading, ModalFooter, Button } from '@godaddy/antares';

export function HorizontalMediaExample() {
return (
<ModalTrigger>
<Button variant="primary">Product details</Button>
<Modal
size="large"
isDismissable
media={<img src="https://picsum.photos/600/800" alt="Product" />}
mediaVariant="full-bleed"
alignment="horizontal"
mediaPosition="start"
>
{({ close }) => (
<>
<ModalHeading>Product details</ModalHeading>
<p>This modal uses a horizontal layout with the media positioned at the start.</p>
<p>The content area scrolls independently while the media stays in place.</p>
<ModalFooter fixedActions>
<Button variant="secondary" onPress={close}>Cancel</Button>
<Button variant="primary" onPress={close}>Add to cart</Button>
</ModalFooter>
</>
)}
</Modal>
</ModalTrigger>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { ModalTrigger, Modal, ModalHeading, ModalFooter, Button } from '@godaddy/antares';
import type { ModalProps } from '@godaddy/antares';

export interface PlaygroundExampleProps {
size?: ModalProps['size'];
isDismissable?: boolean;
mediaVariant?: ModalProps['mediaVariant'];
alignment?: ModalProps['alignment'];
mediaPosition?: ModalProps['mediaPosition'];
textAlign?: ModalProps['textAlign'];
showMedia?: boolean;
fixedActions?: boolean;
}

export function PlaygroundExample({
size,
isDismissable = true,
mediaVariant,
alignment,
mediaPosition,
textAlign,
showMedia,
fixedActions
}: PlaygroundExampleProps) {
return (
<ModalTrigger>
<Button variant="primary">Open modal</Button>
<Modal
size={size}
isDismissable={isDismissable}
media={showMedia ? <img src="https://picsum.photos/800/400" alt="Placeholder" /> : undefined}
mediaVariant={showMedia ? mediaVariant : undefined}
alignment={showMedia ? alignment : undefined}
mediaPosition={showMedia ? mediaPosition : undefined}
textAlign={textAlign}
>
{({ close }) => (
<>
<ModalHeading>Modal title</ModalHeading>
<p>This is the modal content. Adjust the controls to explore different configurations.</p>
<ModalFooter fixedActions={fixedActions}>
<Button variant="secondary" onPress={close}>Cancel</Button>
<Button variant="primary" onPress={close}>Confirm</Button>
</ModalFooter>
</>
)}
</Modal>
</ModalTrigger>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ModalTrigger, Modal, ModalHeading, ModalFooter, Button } from '@godaddy/antares';

export function WithActionsExample() {
return (
<ModalTrigger>
<Button variant="primary">Confirm action</Button>
<Modal isDismissable>
{({ close }) => (
<>
<ModalHeading>Confirm changes</ModalHeading>
<p>Are you sure you want to save these changes? This action cannot be undone.</p>
<ModalFooter>
<Button variant="secondary" onPress={close}>Cancel</Button>
<Button variant="primary" onPress={close}>Save changes</Button>
</ModalFooter>
</>
)}
</Modal>
</ModalTrigger>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { ModalTrigger, Modal, ModalHeading, ModalFooter, Button } from '@godaddy/antares';

export function WithMediaExample() {
return (
<ModalTrigger>
<Button variant="primary">View details</Button>
<Modal
isDismissable
media={<img src="https://picsum.photos/800/300" alt="Placeholder" />}
mediaVariant="inset"
>
{({ close }) => (
<>
<ModalHeading>Featured content</ModalHeading>
<p>This modal includes an inset media element displayed above the content.</p>
<ModalFooter>
<Button variant="primary" onPress={close}>Close</Button>
</ModalFooter>
</>
)}
</Modal>
</ModalTrigger>
);
}
78 changes: 78 additions & 0 deletions packages/@godaddy/antares/components/modal/modal.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
'use client';
import { getMeta, getComponentDocs, getStory } from '@bento/storybook-addon-helpers';
import { Modal } from './src/index.tsx';
import { DefaultExample } from './examples/default.tsx';
import { WithActionsExample } from './examples/with-actions.tsx';
import { WithMediaExample } from './examples/with-media.tsx';
import { HorizontalMediaExample } from './examples/horizontal-media.tsx';
import { FullscreenExample } from './examples/fullscreen.tsx';
import { PlaygroundExample, type PlaygroundExampleProps } from './examples/modal-playground.tsx';

export default getMeta({
title: 'Antares/Components/Modal'
});

export const Props = getComponentDocs(Modal);

export const Default = getStory(DefaultExample);

export const WithActions = getStory(WithActionsExample);

export const WithMedia = getStory(WithMediaExample);

export const HorizontalMedia = getStory(HorizontalMediaExample);

export const Fullscreen = getStory(FullscreenExample);

export const Playground = {
render: (args: PlaygroundExampleProps) => <PlaygroundExample {...args} />,
args: {
size: 'default',
isDismissable: true,
showMedia: false,
mediaVariant: 'inset',
alignment: 'default',
mediaPosition: 'start',
textAlign: 'start',
fixedActions: false
},
argTypes: {
size: {
control: 'radio',
options: ['default', 'large', 'fullscreen'],
description: 'Size of the modal'
},
isDismissable: {
control: 'boolean',
description: 'Whether the modal can be dismissed'
},
showMedia: {
control: 'boolean',
description: 'Show a media element in the modal'
},
mediaVariant: {
control: 'radio',
options: ['inset', 'full-bleed'],
description: 'How the media is displayed'
},
alignment: {
control: 'radio',
options: ['default', 'horizontal'],
description: 'Layout direction when media is present'
},
mediaPosition: {
control: 'radio',
options: ['start', 'end'],
description: 'Which side the media appears on in horizontal layout'
},
textAlign: {
control: 'radio',
options: ['start', 'center'],
description: 'Text alignment within the content area'
},
fixedActions: {
control: 'boolean',
description: 'Whether footer actions are sticky at the bottom'
}
}
};
Loading
Loading