diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/Deck.md b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/Deck.md
new file mode 100644
index 00000000..e10582e6
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/Deck.md
@@ -0,0 +1,45 @@
+---
+# Sidenav top-level section
+# should be the same for all markdown files
+section: Component groups
+subsection: Content containers
+# Sidenav secondary level section
+# should be the same for all markdown files
+id: Deck
+# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility)
+source: react
+# If you use typescript, the name of the interface to display props for
+# These are found through the sourceProps function provided in patternfly-docs.source.js
+propComponents: ['Deck', 'DeckPage', 'DeckButton', 'ModalDeck']
+sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/Deck.md
+---
+
+import Deck from '@patternfly/react-component-groups/dist/dynamic/Deck';
+import { ModalDeck } from '@patternfly/react-component-groups/dist/dynamic/ModalDeck';
+import { FunctionComponent, useState } from 'react';
+
+The **deck** component is a compact, sequential container for presenting a suite of static announcements or an informational walkthrough. It is not intended for task completion or form-filling workflows.
+
+## Examples
+
+### Basic deck
+
+This example demonstrates the basic deck with automatic navigation. Buttons can use the `navigation` prop to automatically handle page changes:
+- `navigation: 'next'` - Advances to the next page
+- `navigation: 'previous'` - Goes back to the previous page
+- `navigation: 'close'` - Triggers the onClose callback
+
+You can also add custom `onClick` handlers for analytics, validation, or other logic. The custom `onClick` will be called **before** the automatic navigation occurs.
+
+```ts file="./DeckExample.tsx"
+
+```
+
+### Modal deck
+
+Display the deck in a modal dialog. The `ModalDeck` component wraps the Deck in a PatternFly Modal without a close button or extra padding, ideal for guided walkthroughs that require user interaction.
+
+```ts file="./ModalDeckExample.tsx"
+
+```
+
diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/DeckDemos.md b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/DeckDemos.md
new file mode 100644
index 00000000..b736193f
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/DeckDemos.md
@@ -0,0 +1,26 @@
+---
+# Sidenav top-level section
+# should be the same for all markdown files
+section: Component groups
+subsection: Content containers
+# Sidenav secondary level section
+# should be the same for all markdown files
+id: Deck
+# Tab (react | react-demos | html | html-demos | design-guidelines | accessibility)
+source: react-demos
+sourceLink: https://github.com/patternfly/react-component-groups/blob/main/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/DeckDemos.md
+---
+import { FunctionComponent, useState } from 'react';
+
+import Deck from '@patternfly/react-component-groups/dist/dynamic/Deck';
+import { ModalDeck } from '@patternfly/react-component-groups/dist/dynamic/ModalDeck';
+
+## Demos
+
+### Onboarding modal deck
+
+A complete onboarding experience using ModalDeck to guide users through key product features. This demo demonstrates a multi-step walkthrough with custom styling, labels, and content.
+
+```ts file="./OnboardingModalDeckDemo.tsx"
+
+```
\ No newline at end of file
diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/DeckExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/DeckExample.tsx
new file mode 100644
index 00000000..39644656
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/DeckExample.tsx
@@ -0,0 +1,87 @@
+/* eslint-disable no-console */
+import { FunctionComponent, useState } from 'react';
+import Deck, { DeckButton } from '@patternfly/react-component-groups/dist/dynamic/Deck';
+import { ButtonVariant } from '@patternfly/react-core';
+
+export const BasicExample: FunctionComponent = () => {
+ const [ deckKey, setDeckKey ] = useState(0);
+
+ // Simulated analytics function
+ const trackEvent = (eventName, data) => {
+ console.log('Analytics:', eventName, data);
+ };
+
+ const restartDeck = () => {
+ setDeckKey(prev => prev + 1);
+ trackEvent('deck_restarted', {});
+ };
+
+ const pages = [
+ {
+ content: (
+
+
This is the first page of your informational walkthrough.
+
+ ),
+ buttons: [
+ {
+ children: 'Next',
+ variant: ButtonVariant.primary,
+ navigation: 'next',
+ // Custom onClick for analytics - called before navigation
+ onClick: () => trackEvent('deck_next_clicked', { fromPage: 1 })
+ }
+ ] as DeckButton[]
+ },
+ {
+ content: (
+
+
Continue through your walkthrough.
+
+ ),
+ buttons: [
+ {
+ children: 'Next',
+ variant: ButtonVariant.primary,
+ navigation: 'next',
+ onClick: () => trackEvent('deck_next_clicked', { fromPage: 2 })
+ }
+ ] as DeckButton[]
+ },
+ {
+ content: (
+
+
You've reached the end of the deck.
+
+ ),
+ buttons: [
+ {
+ children: 'Restart',
+ variant: ButtonVariant.primary,
+ // Restart the deck for demo purposes
+ onClick: () => {
+ trackEvent('deck_completed', { totalPages: 3 });
+ console.log('Deck completed! Restarting...');
+ restartDeck();
+ }
+ }
+ ]
+ }
+ ];
+
+ return (
+ {
+ console.log('Current page:', index);
+ trackEvent('deck_page_changed', { page: index + 1 });
+ }}
+ onClose={() => {
+ trackEvent('deck_closed', {});
+ console.log('Deck closed');
+ }}
+ />
+ );
+};
+
diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/ModalDeckExample.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/ModalDeckExample.tsx
new file mode 100644
index 00000000..5b37e245
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/ModalDeckExample.tsx
@@ -0,0 +1,78 @@
+/* eslint-disable no-console */
+import { FunctionComponent, useState } from 'react';
+import Deck, { DeckButton } from '@patternfly/react-component-groups/dist/dynamic/Deck';
+import { ModalDeck } from '@patternfly/react-component-groups/dist/dynamic/ModalDeck';
+import { Button, ButtonVariant } from '@patternfly/react-core';
+
+export const ModalDeckExample: FunctionComponent = () => {
+ const [ isModalOpen, setIsModalOpen ] = useState(false);
+
+ const handleClose = () => {
+ setIsModalOpen(false);
+ console.log('Modal deck closed');
+ };
+
+ const pages = [
+ {
+ content: (
+
+
Welcome to the Modal Deck
+
This deck is displayed in a modal dialog.
+
+ ),
+ buttons: [
+ {
+ children: 'Next',
+ variant: ButtonVariant.primary,
+ navigation: 'next'
+ }
+ ] as DeckButton[]
+ },
+ {
+ content: (
+
+
Page 2
+
Navigate through the walkthrough.
+
+ ),
+ buttons: [
+ {
+ children: 'Next',
+ variant: ButtonVariant.primary,
+ navigation: 'next'
+ }
+ ] as DeckButton[]
+ },
+ {
+ content: (
+
+
Final Page
+
Click Close to finish.
+
+ ),
+ buttons: [
+ {
+ children: 'Close',
+ variant: ButtonVariant.primary,
+ navigation: 'close'
+ }
+ ] as DeckButton[]
+ }
+ ];
+
+ return (
+ <>
+ setIsModalOpen(true)}>
+ Launch modal deck
+
+
+ console.log('Page changed to:', index + 1)}
+ />
+
+ >
+ );
+};
+
diff --git a/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/OnboardingModalDeckDemo.tsx b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/OnboardingModalDeckDemo.tsx
new file mode 100644
index 00000000..bf94a9cb
--- /dev/null
+++ b/packages/module/patternfly-docs/content/extensions/component-groups/examples/Deck/OnboardingModalDeckDemo.tsx
@@ -0,0 +1,130 @@
+/* eslint-disable no-console */
+import { FunctionComponent, useState } from 'react';
+import Deck, { DeckButton } from '@patternfly/react-component-groups/dist/dynamic/Deck';
+import { ModalDeck } from '@patternfly/react-component-groups/dist/dynamic/ModalDeck';
+import { Button, ButtonVariant, Label, Title, Stack, StackItem, Content } from '@patternfly/react-core';
+
+export const OnboardingModalDeckDemo: FunctionComponent = () => {
+ const [ isModalOpen, setIsModalOpen ] = useState(false);
+
+ const handleClose = () => {
+ setIsModalOpen(false);
+ console.log('Onboarding completed or skipped');
+ };
+
+ // Placeholder for illustration - in a real app, replace with actual images
+ const placeholderImage = (
+
+ );
+
+ const pages = [
+ {
+ content: (
+
+ {placeholderImage}
+
+ Welcome to [Product Name]
+
+
+
+ Harness the full potential of the hybrid cloud, simply by asking.
+
+
+
+ ),
+ buttons: [
+ {
+ children: 'Continue',
+ variant: ButtonVariant.primary,
+ navigation: 'next'
+ }
+ ] as DeckButton[]
+ },
+ {
+ content: (
+
+ {placeholderImage}
+ AI Command Center
+ Intelligence at your command
+ Ask anything. Get answers. Troubleshoot, analyze, and understand your entire fleet just by asking. It's the power of your data, in plain language.
+
+ ),
+ buttons: [
+ {
+ children: 'Continue',
+ variant: ButtonVariant.primary,
+ navigation: 'next'
+ }
+ ] as DeckButton[]
+ },
+ {
+ content: (
+
+ {placeholderImage}
+ Canvas Mode
+ Go from conversation to clarity.
+ Transform answers into custom dashboards. In Canvas Mode, you can effortlessly arrange, customize, and build the precise view you need to monitor what matters most.
+
+ ),
+ buttons: [
+ {
+ children: 'Continue',
+ variant: ButtonVariant.primary,
+ navigation: 'next'
+ }
+ ] as DeckButton[]
+ },
+ {
+ content: (
+
+ {placeholderImage}
+ Sharing
+ Share your vision. Instantly.
+ An insight is only powerful when it’s shared. Save any view to your library and share it with your team in a single click. Drive decisions, together.
+
+ ),
+ buttons: [
+ {
+ children: 'Get started',
+ variant: ButtonVariant.primary,
+ navigation: 'close'
+ }
+ ] as DeckButton[]
+ }
+ ];
+
+ return (
+ <>
+ setIsModalOpen(true)}>
+ Launch onboarding
+
+
+ console.log('Onboarding page:', index + 1)}
+ ariaLabel="Product onboarding"
+ ariaRoleDescription="onboarding walkthrough"
+ contentFlexProps={{
+ spaceItems: { default: 'spaceItemsXl' }
+ }}
+ />
+
+ >
+ );
+};
+
diff --git a/packages/module/src/Deck/Deck.test.tsx b/packages/module/src/Deck/Deck.test.tsx
new file mode 100644
index 00000000..bd298498
--- /dev/null
+++ b/packages/module/src/Deck/Deck.test.tsx
@@ -0,0 +1,263 @@
+import { render, screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import Deck, { DeckButton } from './Deck';
+import { ButtonVariant } from '@patternfly/react-core';
+
+describe('Deck component', () => {
+ const mockPages = [
+ {
+ content: Page 1 content
,
+ buttons: [
+ {
+ children: 'Next',
+ variant: ButtonVariant.primary,
+ navigation: 'next' as const
+ }
+ ] as DeckButton[]
+ },
+ {
+ content: Page 2 content
,
+ buttons: [
+ {
+ children: 'Previous',
+ variant: ButtonVariant.secondary,
+ navigation: 'previous' as const
+ },
+ {
+ children: 'Next',
+ variant: ButtonVariant.primary,
+ navigation: 'next' as const
+ }
+ ] as DeckButton[]
+ },
+ {
+ content: Page 3 content
,
+ buttons: [
+ {
+ children: 'Close',
+ variant: ButtonVariant.primary,
+ navigation: 'close' as const
+ }
+ ] as DeckButton[]
+ }
+ ];
+
+ test('should render with basic pages', () => {
+ const pages = [
+ {
+ content: Page 1 content
,
+ buttons: []
+ }
+ ];
+
+ const { container } = render( );
+ expect(container).toMatchSnapshot();
+ });
+
+ test('should render first page content by default', () => {
+ render( );
+ expect(screen.getByText('Page 1 content')).toBeInTheDocument();
+ });
+
+ test('should render with hideProgressDots', () => {
+ const pages = [
+ {
+ content: Page 1 content
,
+ buttons: []
+ }
+ ];
+
+ const { container } = render(
+
+ );
+
+ const progressDots = container.querySelector('[data-ouia-component-id="Deck-progress-dots"]');
+ expect(progressDots).not.toBeInTheDocument();
+ });
+
+ test('should not render progress dots when only one page', () => {
+ const pages = [
+ {
+ content: Single page
,
+ buttons: []
+ }
+ ];
+
+ const { container } = render( );
+ const progressDots = container.querySelector('[data-ouia-component-id="Deck-progress-dots"]');
+ expect(progressDots).not.toBeInTheDocument();
+ });
+
+ test('should navigate to next page when next button is clicked', async () => {
+ const user = userEvent.setup();
+ render( );
+
+ expect(screen.getByText('Page 1 content')).toBeInTheDocument();
+
+ const nextButton = screen.getByRole('button', { name: 'Next' });
+ await user.click(nextButton);
+
+ expect(screen.getByText('Page 2 content')).toBeInTheDocument();
+ });
+
+ test('should navigate to previous page when previous button is clicked', async () => {
+ const user = userEvent.setup();
+ render( );
+
+ expect(screen.getByText('Page 2 content')).toBeInTheDocument();
+
+ const previousButton = screen.getByRole('button', { name: 'Previous' });
+ await user.click(previousButton);
+
+ expect(screen.getByText('Page 1 content')).toBeInTheDocument();
+ });
+
+ test('should call onClose when close button is clicked', async () => {
+ const user = userEvent.setup();
+ const onClose = jest.fn();
+
+ render( );
+
+ const closeButton = screen.getByRole('button', { name: 'Close' });
+ await user.click(closeButton);
+
+ expect(onClose).toHaveBeenCalledTimes(1);
+ });
+
+ test('should call onPageChange callback when page changes', async () => {
+ const user = userEvent.setup();
+ const onPageChange = jest.fn();
+
+ render( );
+
+ const nextButton = screen.getByRole('button', { name: 'Next' });
+ await user.click(nextButton);
+
+ expect(onPageChange).toHaveBeenCalledWith(1);
+ });
+
+ test('should call custom onClick before navigation', async () => {
+ const user = userEvent.setup();
+ const customOnClick = jest.fn();
+
+ const pagesWithCustomClick = [
+ {
+ content: Page 1
,
+ buttons: [
+ {
+ children: 'Next',
+ variant: ButtonVariant.primary,
+ navigation: 'next' as const,
+ onClick: customOnClick
+ }
+ ] as DeckButton[]
+ },
+ {
+ content: Page 2
,
+ buttons: []
+ }
+ ];
+
+ render( );
+
+ const nextButton = screen.getByRole('button', { name: 'Next' });
+ await user.click(nextButton);
+
+ expect(customOnClick).toHaveBeenCalledTimes(1);
+ expect(screen.getByText('Page 2')).toBeInTheDocument();
+ });
+
+ test('should start at initialPage', () => {
+ render( );
+ expect(screen.getByText('Page 2 content')).toBeInTheDocument();
+ });
+
+ test('should render with custom OUIA ID', () => {
+ const { container } = render( );
+ const element = container.querySelector('[data-ouia-component-id="CustomDeck"]');
+ expect(element).toBeInTheDocument();
+ });
+
+ test('should apply custom text alignment class', () => {
+ const { container } = render( );
+ const contentElement = container.querySelector('[data-ouia-component-id="Deck-content"]');
+ expect(contentElement).toHaveClass('pf-v6-u-text-align-left');
+ });
+
+ test('should not apply text alignment class when textAlign is false', () => {
+ const { container } = render( );
+ const contentElement = container.querySelector('[data-ouia-component-id="Deck-content"]');
+ expect(contentElement).not.toHaveClass('pf-v6-u-text-align-center');
+ });
+
+ test('should render with custom aria labels', () => {
+ const { container } = render(
+
+ );
+
+ const region = container.querySelector('[role="region"]');
+ expect(region).toHaveAttribute('aria-label', 'Custom deck label');
+ expect(region).toHaveAttribute('aria-roledescription', 'Custom role description');
+ });
+
+ test('should render accessible page info for screen readers', () => {
+ const { container } = render( );
+ const pageInfo = container.querySelector('.pf-v6-screen-reader');
+ expect(pageInfo).toBeInTheDocument();
+ expect(pageInfo).toHaveTextContent('Page 1 of 3');
+ });
+
+ test('should update page info when page changes', async () => {
+ const user = userEvent.setup();
+ const { container } = render( );
+
+ const nextButton = screen.getByRole('button', { name: 'Next' });
+ await user.click(nextButton);
+
+ const pageInfo = container.querySelector('.pf-v6-screen-reader');
+ expect(pageInfo).toHaveTextContent('Page 2 of 3');
+ });
+
+ test('should use custom getPageLabel function', () => {
+ const getPageLabel = (current: number, total: number) => `Step ${current}/${total}`;
+ const { container } = render(
+
+ );
+
+ const pageInfo = container.querySelector('.pf-v6-screen-reader');
+ expect(pageInfo).toHaveTextContent('Step 1/3');
+ });
+
+ test('should render correct number of progress dots', () => {
+ const { container } = render( );
+ const dots = container.querySelectorAll('[data-ouia-component-id="Deck-progress-dots"] span[aria-hidden="true"]');
+ expect(dots).toHaveLength(3);
+ });
+
+ test('should not navigate beyond last page', async () => {
+ const onPageChange = jest.fn();
+
+ render( );
+
+ expect(screen.getByText('Page 3 content')).toBeInTheDocument();
+
+ // There's no next button on the last page in our mock, so the page shouldn't change
+ // This tests the boundary condition
+ expect(onPageChange).not.toHaveBeenCalled();
+ });
+
+ test('should not navigate before first page', async () => {
+ const onPageChange = jest.fn();
+
+ render( );
+
+ expect(screen.getByText('Page 1 content')).toBeInTheDocument();
+
+ // There's no previous button on the first page in our mock
+ expect(onPageChange).not.toHaveBeenCalled();
+ });
+});
diff --git a/packages/module/src/Deck/Deck.tsx b/packages/module/src/Deck/Deck.tsx
new file mode 100644
index 00000000..89ed6f0a
--- /dev/null
+++ b/packages/module/src/Deck/Deck.tsx
@@ -0,0 +1,183 @@
+import type { FunctionComponent, ReactNode } from 'react';
+import { useState } from 'react';
+import { ActionList, ActionListItem, ActionListGroup, Bullseye, Button, ButtonProps, Flex, FlexItem, FlexProps } from '@patternfly/react-core';
+
+/**
+ * extends PatternFly's ButtonProps
+ */
+export interface DeckButton extends ButtonProps {
+ /** Automatically navigate to next/previous page or close the deck when clicked */
+ navigation?: 'next' | 'previous' | 'close';
+}
+
+export interface DeckPage {
+ /** Content to display on this page */
+ content: ReactNode;
+ /** Array of button configurations for this page */
+ buttons?: DeckButton[];
+}
+
+export interface DeckProps {
+ /** Array of pages to display in the deck */
+ pages: DeckPage[];
+ /** Deck className */
+ className?: string;
+ /** Custom OUIA ID */
+ ouiaId?: string;
+ /** Hide the progress dots indicator */
+ hideProgressDots?: boolean;
+ /** Initial page index to display (0-based) */
+ initialPage?: number;
+ /** Callback when page changes */
+ onPageChange?: (pageIndex: number) => void;
+ /** Callback when deck is closed/cancelled */
+ onClose?: () => void;
+ /** Additional props for the Flex layout containing content, progress dots, and buttons */
+ contentFlexProps?: FlexProps;
+ /** Text alignment for content (uses PatternFly utility classes). Set to false to disable. */
+ textAlign?: 'center' | 'left' | 'right' | false;
+ /** Accessible label for the deck region */
+ ariaLabel?: string;
+ /** Accessible role description for the deck */
+ ariaRoleDescription?: string;
+ /** Function to generate accessible page info label. Receives (currentPage, totalPages) and returns a string. */
+ getPageLabel?: (currentPage: number, totalPages: number) => string;
+}
+
+export const Deck: FunctionComponent = ({
+ pages,
+ className,
+ ouiaId = 'Deck',
+ hideProgressDots = false,
+ initialPage = 0,
+ onPageChange,
+ onClose,
+ contentFlexProps,
+ textAlign = 'center',
+ ariaLabel = 'Information deck',
+ ariaRoleDescription = 'sequential information deck',
+ getPageLabel = (current, total) => `Page ${current} of ${total}`,
+ ...props
+}: DeckProps) => {
+ const [ currentPageIndex, setCurrentPageIndex ] = useState(initialPage);
+
+ const handlePageChange = (newIndex: number) => {
+ if (newIndex >= 0 && newIndex < pages.length) {
+ setCurrentPageIndex(newIndex);
+ onPageChange?.(newIndex);
+ }
+ };
+
+ const currentPage = pages[currentPageIndex];
+
+ // Generate text alignment class if specified
+ const textAlignClass = textAlign ? `pf-v6-u-text-align-${textAlign}` : '';
+
+ // Generate accessible label with page information
+ const pageInfo = getPageLabel(currentPageIndex + 1, pages.length);
+ const pageInfoId = `${ouiaId}-page-info`;
+
+ return (
+
+
+ {/* Visually hidden page info for screen readers */}
+
+ {pageInfo}
+
+
+ {/* Current page content */}
+
+ {currentPage?.content}
+
+
+ {/* Progress dots */}
+ {!hideProgressDots && pages.length > 1 && (
+
+
+ {pages.map((_, index) => (
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Page buttons */}
+ {currentPage?.buttons && currentPage.buttons.length > 0 && (
+
+
+
+ {currentPage.buttons.map((buttonConfig, index) => {
+ const { navigation, onClick, ...buttonProps } = buttonConfig;
+
+ // Auto-wire navigation if specified
+ const handleClick = (event: React.MouseEvent) => {
+ // Call user's custom onClick first if provided
+ onClick?.(event);
+
+ // Then handle navigation
+ if (navigation === 'next') {
+ handlePageChange(currentPageIndex + 1);
+ } else if (navigation === 'previous') {
+ handlePageChange(currentPageIndex - 1);
+ } else if (navigation === 'close') {
+ onClose?.();
+ }
+ };
+
+ return (
+
+
+
+ );
+ })}
+
+
+
+ )}
+
+
+ );
+};
+
+export default Deck;
diff --git a/packages/module/src/Deck/__snapshots__/Deck.test.tsx.snap b/packages/module/src/Deck/__snapshots__/Deck.test.tsx.snap
new file mode 100644
index 00000000..b902c63b
--- /dev/null
+++ b/packages/module/src/Deck/__snapshots__/Deck.test.tsx.snap
@@ -0,0 +1,35 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Deck component should render with basic pages 1`] = `
+
+`;
diff --git a/packages/module/src/Deck/index.ts b/packages/module/src/Deck/index.ts
new file mode 100644
index 00000000..4b8d89ad
--- /dev/null
+++ b/packages/module/src/Deck/index.ts
@@ -0,0 +1,3 @@
+export { default } from './Deck';
+export * from './Deck';
+
diff --git a/packages/module/src/ModalDeck/ModalDeck.test.tsx b/packages/module/src/ModalDeck/ModalDeck.test.tsx
new file mode 100644
index 00000000..4c4cb474
--- /dev/null
+++ b/packages/module/src/ModalDeck/ModalDeck.test.tsx
@@ -0,0 +1,103 @@
+import { render, screen } from '@testing-library/react';
+import { ModalDeck } from './ModalDeck';
+import { Button } from '@patternfly/react-core';
+
+describe('ModalDeck component', () => {
+ test('should render when open', () => {
+ const { container } = render(
+
+
+ Modal content
+ Close
+
+
+ );
+
+ expect(screen.getByText('Modal content')).toBeInTheDocument();
+ expect(container).toMatchSnapshot();
+ });
+
+ test('should not render when closed', () => {
+ render(
+
+
+ Modal content
+ Close
+
+
+ );
+
+ expect(screen.queryByText('Modal content')).not.toBeInTheDocument();
+ });
+
+ test('should render children', () => {
+ render(
+
+
+ Test content
+ Action
+
+
+ );
+
+ expect(screen.getByTestId('test-child')).toBeInTheDocument();
+ expect(screen.getByText('Test content')).toBeInTheDocument();
+ });
+
+ test('should apply small variant by default', () => {
+ render(
+
+
+ Content
+ Close
+
+
+ );
+
+ const modal = screen.getByRole('dialog');
+ expect(modal).toBeInTheDocument();
+ // Small variant is the default, just verify modal renders
+ });
+
+ test('should pass through modalProps', () => {
+ render(
+
+
+ Content
+ Close
+
+
+ );
+
+ const modal = screen.getByRole('dialog');
+ expect(modal).toHaveAttribute('aria-label', 'Custom modal label');
+ expect(modal).toHaveAttribute('aria-describedby', 'custom-description');
+ });
+
+ test('should override variant through modalProps', () => {
+ render(
+
+
+ Content
+ Close
+
+
+ );
+
+ const modal = screen.getByRole('dialog');
+ expect(modal).toBeInTheDocument();
+ // Variant override is passed through modalProps, just verify modal renders
+ });
+});
+
diff --git a/packages/module/src/ModalDeck/ModalDeck.tsx b/packages/module/src/ModalDeck/ModalDeck.tsx
new file mode 100644
index 00000000..11e4a801
--- /dev/null
+++ b/packages/module/src/ModalDeck/ModalDeck.tsx
@@ -0,0 +1,30 @@
+import type { FunctionComponent, ReactNode } from 'react';
+import { Modal, ModalBody, ModalProps } from '@patternfly/react-core';
+
+export interface ModalDeckProps {
+ /** Whether the modal is open */
+ isOpen: boolean;
+ /** Deck component to display in the modal */
+ children: ReactNode;
+ /** Additional Modal props */
+ modalProps?: Omit;
+}
+
+export const ModalDeck: FunctionComponent = ({
+ isOpen,
+ children,
+ modalProps
+}: ModalDeckProps) => (
+
+
+ {children}
+
+
+);
+
+export default ModalDeck;
+
diff --git a/packages/module/src/ModalDeck/__snapshots__/ModalDeck.test.tsx.snap b/packages/module/src/ModalDeck/__snapshots__/ModalDeck.test.tsx.snap
new file mode 100644
index 00000000..49048f97
--- /dev/null
+++ b/packages/module/src/ModalDeck/__snapshots__/ModalDeck.test.tsx.snap
@@ -0,0 +1,7 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ModalDeck component should render when open 1`] = `
+
+`;
diff --git a/packages/module/src/ModalDeck/index.ts b/packages/module/src/ModalDeck/index.ts
new file mode 100644
index 00000000..b673d7ba
--- /dev/null
+++ b/packages/module/src/ModalDeck/index.ts
@@ -0,0 +1,3 @@
+export { default } from './ModalDeck';
+export * from './ModalDeck';
+
diff --git a/packages/module/src/index.ts b/packages/module/src/index.ts
index 0b16baee..4f23c2d4 100644
--- a/packages/module/src/index.ts
+++ b/packages/module/src/index.ts
@@ -51,6 +51,9 @@ export * from './NotFoundIcon';
export { default as MultiContentCard } from './MultiContentCard';
export * from './MultiContentCard';
+export { default as ModalDeck } from './ModalDeck';
+export * from './ModalDeck';
+
export { default as MissingPage } from './MissingPage';
export * from './MissingPage';
@@ -78,6 +81,9 @@ export * from './ErrorStack';
export { default as ErrorBoundary } from './ErrorBoundary';
export * from './ErrorBoundary';
+export { default as Deck } from './Deck';
+export * from './Deck';
+
export { default as ColumnManagementModal } from './ColumnManagementModal';
export * from './ColumnManagementModal';