diff --git a/lib/components/Snackbar/SnackbarWrapper.tsx b/lib/components/Snackbar/SnackbarWrapper.tsx
new file mode 100644
index 00000000..b9364a42
--- /dev/null
+++ b/lib/components/Snackbar/SnackbarWrapper.tsx
@@ -0,0 +1,42 @@
+import { StandalonePlaceholderRegularIcon } from "@deriv/quill-icons/Standalone";
+import { Snackbar } from ".";
+import React, { useState } from "react";
+
+export const SnackbarWrapper = () => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ const handleActionClick = () => {
+ handleClose();
+ };
+
+ const handleOpen = () => {
+ setIsOpen(true);
+ };
+
+ const handleClose = () => {
+ setTimeout(() => {
+ setIsOpen(false);
+ }, 1000);
+ };
+
+ return (
+
+
+
+ }
+ message="Enter message here"
+ actionText="Action"
+ onActionClick={handleActionClick}
+ isOpen={isOpen}
+ onClose={handleClose}
+ />
+
+ );
+};
diff --git a/lib/components/Snackbar/__tests__/__snapshots__/snackbar.test.tsx.snap b/lib/components/Snackbar/__tests__/__snapshots__/snackbar.test.tsx.snap
new file mode 100644
index 00000000..f9bcb1b1
--- /dev/null
+++ b/lib/components/Snackbar/__tests__/__snapshots__/snackbar.test.tsx.snap
@@ -0,0 +1,365 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Snackbar calls onActionClick when action button is clicked 1`] = `
+
+
+
+
+
+
+
+`;
+
+exports[`Snackbar calls onClose after a certain duration when Snackbar is open 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`Snackbar calls onClose when close button is clicked 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`Snackbar renders correctly with custom icon 1`] = `
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Snackbar renders correctly without action button 1`] = `
+
+
+
+
+
+
+`;
+
+exports[`Snackbar renders correctly without close button 1`] = `
+
+`;
+
+exports[`Snackbar renders with default props 1`] = `
+
+
+
+
+
+
+`;
diff --git a/lib/components/Snackbar/__tests__/snackbar.test.tsx b/lib/components/Snackbar/__tests__/snackbar.test.tsx
new file mode 100644
index 00000000..1cd906c8
--- /dev/null
+++ b/lib/components/Snackbar/__tests__/snackbar.test.tsx
@@ -0,0 +1,61 @@
+import { render, screen, fireEvent } from "@testing-library/react";
+import { Snackbar } from "..";
+
+describe("Snackbar", () => {
+ const testMessage = "test message"
+ const defaultProps = {
+ message: testMessage,
+ isOpen: true,
+ onClose: jest.fn(),
+ };
+
+ const renderComponent = (props = {}) =>
+ render();
+
+ it("renders with default props", () => {
+ const { container } = renderComponent();
+ expect(screen.getByText(testMessage)).toBeInTheDocument();
+ expect(container).toMatchSnapshot();
+ });
+ it("calls onActionClick when action button is clicked", () => {
+ const onActionClickMock = jest.fn();
+ const { container } = renderComponent({
+ actionText: "Action",
+ onActionClick: onActionClickMock,
+ });
+ fireEvent.click(screen.getByText("Action"));
+ expect(onActionClickMock).toHaveBeenCalled();
+ expect(container).toMatchSnapshot();
+ });
+ it("calls onClose when close button is clicked", () => {
+ const { container } = renderComponent();
+ fireEvent.click(screen.getByTestId('close-button'));
+ expect(defaultProps.onClose).toHaveBeenCalled();
+ expect(container).toMatchSnapshot();
+ });
+ it("calls onClose after a certain duration when Snackbar is open", () => {
+ jest.useFakeTimers();
+ const { container } = renderComponent();
+ jest.advanceTimersByTime(3000);
+ expect(defaultProps.onClose).toHaveBeenCalled();
+ jest.useRealTimers();
+ expect(container).toMatchSnapshot();
+ });
+ it("renders correctly with custom icon", () => {
+ const { container } = renderComponent({
+ icon: ,
+ });
+ expect(screen.getByAltText("Custom Icon")).toBeInTheDocument();
+ expect(container).toMatchSnapshot();
+ });
+ it("renders correctly without action button", () => {
+ const { container } = renderComponent();
+ expect(screen.queryByText("Action")).not.toBeInTheDocument();
+ expect(container).toMatchSnapshot();
+ });
+ it("renders correctly without close button", () => {
+ const { container } = renderComponent({ hasCloseButton: false });
+ expect(screen.queryByText("x")).not.toBeInTheDocument();
+ expect(container).toMatchSnapshot();
+ });
+});
diff --git a/lib/components/Snackbar/index.tsx b/lib/components/Snackbar/index.tsx
new file mode 100644
index 00000000..e625daed
--- /dev/null
+++ b/lib/components/Snackbar/index.tsx
@@ -0,0 +1,86 @@
+import React, { ReactNode, useEffect, useState, HTMLAttributes } from "react";
+import { Text } from "../Typography";
+import "./snackbar.scss";
+import clsx from "clsx";
+import { Button } from "../Button";
+import { LabelPairedXmarkSmBoldIcon } from "@deriv/quill-icons";
+
+interface SnackbarProps extends HTMLAttributes {
+ icon?: ReactNode;
+ message: string;
+ actionText?: string;
+ hasCloseButton?: boolean;
+ onClose: () => void;
+ onActionClick?: () => void;
+ isOpen: boolean;
+}
+
+export const Snackbar = ({
+ icon: Icon,
+ message,
+ actionText,
+ onActionClick,
+ hasCloseButton = true,
+ isOpen = false,
+ onClose,
+ ...rest
+}: SnackbarProps) => {
+ const animationSpeedObj = Object.freeze({
+ fast: 'fast',
+ slow: 'slow'
+ });
+
+ const [animationSpeed, setAnimationSpeed] = useState(animationSpeedObj.slow);
+ useEffect(() => {
+ if (isOpen) {
+ setAnimationSpeed(animationSpeedObj.slow);
+ const timer = setTimeout(() => {
+ onClose?.();
+ }, 3000);
+
+ return () => {
+ clearTimeout(timer);
+ };
+ }
+ }, [isOpen]);
+
+ const handleClose = () => {
+ onClose?.();
+ setAnimationSpeed(animationSpeedObj.fast);
+ };
+
+ const handleActionClick = () => {
+ onActionClick?.();
+ setAnimationSpeed(animationSpeedObj.fast);
+ };
+ return (
+ <>
+ {isOpen && (
+
+ {Icon && (
+
{Icon}
+ )}
+
+
+ {message}
+
+
+ {actionText && (
+
+ )}
+ {hasCloseButton && (
+
} color="white" onClick={handleClose} data-testid='close-button' />
+ )}
+
+ )}
+ >
+ );
+};
diff --git a/lib/components/Snackbar/snackbar.scss b/lib/components/Snackbar/snackbar.scss
new file mode 100644
index 00000000..2551a785
--- /dev/null
+++ b/lib/components/Snackbar/snackbar.scss
@@ -0,0 +1,102 @@
+@import "@quill/spacing.scss";
+@import "@quill/border.scss";
+@import "@quill/breakpoints.scss";
+@import "@quill/motion.scss";
+@import "@quill/elevation.scss";
+@import "@quill/color.opacity.scss";
+
+.snackbar {
+ display: flex;
+ width: calc(100vw - var(--core-spacing-1600));
+ max-width: 560px;
+ min-height: var(--core-size-2400);
+ padding: var(--semantic-spacing-general-sm)
+ var(--semantic-spacing-general-md) var(--semantic-spacing-general-sm);
+ align-items: center;
+ gap: var(--semantic-spacing-gap-md);
+ border-radius: var(--semantic-borderRadius-md);
+ border: var(--borderWidth-xs) solid
+ var(--core-color-opacity-white-100);
+ background: var(--core-color-solid-slate-1400);
+ box-shadow: var(--core-elevation-shadow-330);
+ position: absolute;
+ z-index: 1;
+ bottom: -100%;
+ transition: bottom var(--motion-duration-snappy)
+ var(--motion-easing-inandout);
+
+ &.slow-animation {
+ animation:
+ slide-up var(--motion-duration-snappy) var(--motion-easing-inandout)
+ forwards,
+ slide-down var(--motion-duration-snappy) var(--core-motion-duration-1300)
+ var(--motion-easing-inandout) forwards;
+ }
+ &.fast-animation {
+ animation:
+ slide-up var(--motion-duration-snappy) var(--motion-easing-inandout)
+ forwards,
+ slide-down var(--motion-duration-snappy) var(--core-motion-duration-800)
+ var(--motion-easing-inandout) forwards;
+ }
+
+ @include breakpoint("sm") {
+ display: inline-flex;
+ width: 560px;
+ }
+
+ &__icon--container {
+ display: flex;
+ width: var(--core-spacing-1200);
+ height: var(--core-spacing-1200);
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ flex-shrink: 0;
+ }
+ &__message {
+ display: -webkit-box;
+ -webkit-box-orient: vertical;
+ -webkit-line-clamp: 2;
+ flex: 1 0 0;
+
+ text-overflow: ellipsis;
+ overflow: hidden;
+ word-wrap: break-word; /* Text gets broken up into another line for long words */
+ color: var(--core-color-solid-slate-50);
+
+ @include breakpoint("sm") {
+ -webkit-line-clamp: 1;
+ }
+
+ &--container {
+ display: flex;
+ padding-right: var(--semantic-spacing-general-sm);
+ align-items: flex-start;
+ flex: 1 0 0;
+ min-width: var(--semantic-spacing-general-none); /* Text broken up into another line for long words */
+ }
+ }
+
+ & .button-label svg {
+ display: flex;
+ }
+}
+
+@keyframes slide-up {
+ 0% {
+ bottom: -100%;
+ }
+ 100% {
+ bottom: var(--core-size-1600);
+ }
+}
+
+@keyframes slide-down {
+ 0% {
+ bottom: var(--core-size-1600);
+ }
+ 100% {
+ bottom: -100%;
+ }
+}
diff --git a/lib/components/Snackbar/snackbar.stories.tsx b/lib/components/Snackbar/snackbar.stories.tsx
new file mode 100644
index 00000000..ec95789e
--- /dev/null
+++ b/lib/components/Snackbar/snackbar.stories.tsx
@@ -0,0 +1,139 @@
+import type { Meta, StoryObj } from "@storybook/react";
+import { StandalonePlaceholderRegularIcon } from "@deriv/quill-icons/Standalone";
+import { fn } from "@storybook/test";
+import { Snackbar } from ".";
+
+const meta = {
+ title: "Components/Snackbar/Snackbar",
+ component: Snackbar,
+ // This component will have an automatically generated Autodocs entry: https://storybook.js.org/docs/writing-docs/autodocs
+ tags: ["autodocs"],
+ args: {
+ icon: ,
+ message: "Message goes here",
+ actionText: "Action",
+ isOpen: false,
+ onActionClick: fn(),
+ onClose: fn(),
+ hasCloseButton: true,
+ },
+ argTypes: {
+ icon: {
+ description:
+ "Optional. There will be no icon if icon is not provided.",
+ table: { type: { summary: "Reactnode | undefined" } },
+ },
+ message: {
+ control: "text",
+ description: "Required. Enter any message.",
+ },
+ actionText: {
+ control: "text",
+ description:
+ "Optional. Action button will not be included if actionText is null or an empty string.",
+ table: { type: { summary: "string | undefined" } },
+ },
+ onActionClick: {
+ description:
+ "Optional. Must be included if we have actionText to trigger action and close snackbar.",
+ },
+ isOpen: {
+ description:
+ "Required. Controls opening and closing of snackbar. Should be set to false by default.",
+ control: "boolean",
+ },
+ onClose: {
+ description:
+ "Required. Needed to close snackbar after a certain time.",
+ },
+ hasCloseButton: {
+ control: "boolean",
+ description: "Optional. Set to true by default.",
+ },
+ },
+} satisfies Meta;
+
+export default meta;
+
+type Story = StoryObj;
+
+export const defaultSnackbar: Story = {
+ parameters: {
+ docs: {
+ story: {
+ height: '100px',
+ }
+ }
+ }
+};
+
+export const SnackbarWithMessageOnly: Story = {
+ args: {
+ isOpen: true,
+ icon: "",
+ hasCloseButton: false,
+ actionText: "",
+ },
+};
+
+export const SnackbarWithIcon: Story = {
+ args: {
+ isOpen: true,
+ icon: ,
+ hasCloseButton: false,
+ actionText: "",
+ },
+};
+
+export const SnackbarWithAction: Story = {
+ args: {
+ isOpen: true,
+ icon: "",
+ hasCloseButton: false,
+ actionText: "Action",
+ onActionClick: fn(),
+ },
+};
+
+export const SnackbarWithCloseButton: Story = {
+ args: {
+ isOpen: true,
+ icon: "",
+ actionText: "",
+ onClose: fn(),
+ },
+};
+
+export const SnackbarWithTwoLinesMessage: Story = {
+ args: {
+ isOpen: true,
+ icon: "",
+ message:
+ "This is an extremely long text that goes on another line. Lorem ipsum lorem lorem. Lorem ipsum lorem lorem. Lorem ipsum lorem lorem.",
+ hasCloseButton: false,
+ actionText: "",
+ },
+};
+
+export const SnackbarWithTwoLinesMessageWithCloseButton: Story = {
+ args: {
+ isOpen: true,
+ icon: "",
+ message:
+ "This is an extremely long text that goes on another line. Lorem ipsum lorem lorem.",
+ onClose: fn(),
+ actionText: "",
+ },
+};
+
+export const SnackbarWithTwoLinesMessageWithActionButton: Story = {
+ args: {
+ isOpen: true,
+ icon: "",
+ message:
+ "This is an extremely long text that goes on another line. Lorem ipsum lorem lorem.",
+ actionText: "Action",
+ hasCloseButton: false,
+ onActionClick: fn(),
+ },
+};
diff --git a/lib/styles/quill/breakpoints.scss b/lib/styles/quill/breakpoints.scss
index 0e411960..871c9fb9 100644
--- a/lib/styles/quill/breakpoints.scss
+++ b/lib/styles/quill/breakpoints.scss
@@ -19,7 +19,8 @@
@media (min-width: 1440px) {
@content;
}
- } @else {
+ }
+ @else {
@warn "Unknown breakpoint: #{$breakpoint}.";
}
}