From 671f2f01f995a5e80677bd8a74ff10dc5d838239 Mon Sep 17 00:00:00 2001 From: Alex Huaracha <129701149+Alex-Huaracha@users.noreply.github.com> Date: Mon, 16 Mar 2026 16:34:57 -0500 Subject: [PATCH] feat: add closeButtonPosition prop to customize close button placement Closes #747 --- src/index.tsx | 9 ++++++++- src/styles.css | 14 ++++++++++++++ src/types.ts | 5 +++++ test/src/app/page.tsx | 1 + test/tests/basic.spec.ts | 13 +++++++++++++ 5 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/index.tsx b/src/index.tsx index ce031e8..52f567b 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -76,6 +76,7 @@ const Toast = (props: ToastProps) => { removeToast, defaultRichColors, closeButton: closeButtonFromToaster, + closeButtonPosition: closeButtonPositionFromToaster, style, cancelButtonStyle, actionButtonStyle, @@ -116,6 +117,10 @@ const Toast = (props: ToastProps) => { () => toast.closeButton ?? closeButtonFromToaster, [toast.closeButton, closeButtonFromToaster], ); + const closeButtonPosition = React.useMemo( + () => toast.closeButtonPosition ?? closeButtonPositionFromToaster ?? 'left', + [toast.closeButtonPosition, closeButtonPositionFromToaster], + ); const duration = React.useMemo( () => toast.duration || durationFromToaster || TOAST_LIFETIME, [toast.duration, durationFromToaster], @@ -421,6 +426,7 @@ const Toast = (props: ToastProps) => { aria-label={closeButtonAriaLabel} data-disabled={disabled} data-close-button + data-close-button-position={closeButtonPosition} onClick={ disabled || !dismissible ? () => {} @@ -860,6 +866,7 @@ const Toaster = React.forwardRef(function Toaster(pro invert={invert} visibleToasts={visibleToasts} closeButton={toastOptions?.closeButton ?? closeButton} + closeButtonPosition={toastOptions?.closeButtonPosition ?? props.closeButtonPosition} interacting={interacting} position={position} style={toastOptions?.style} @@ -886,4 +893,4 @@ const Toaster = React.forwardRef(function Toaster(pro }); export { toast, Toaster, type ExternalToast, type ToastT, type ToasterProps, useSonner }; -export { type ToastClassnames, type ToastToDismiss, type Action } from './types'; +export { type ToastClassnames, type ToastToDismiss, type Action, type CloseButtonPosition } from './types'; diff --git a/src/styles.css b/src/styles.css index ae63e40..5dc0ea3 100644 --- a/src/styles.css +++ b/src/styles.css @@ -241,6 +241,20 @@ html[dir='rtl'], box-shadow: 0px 4px 12px rgba(0, 0, 0, 0.1), 0 0 0 2px rgba(0, 0, 0, 0.2); } +[data-sonner-toast][data-styled='true'] [data-close-button][data-close-button-position='right'] { + left: unset; + right: 0; + transform: translate(35%, -35%); +} + +[data-sonner-toaster][dir='rtl'] + [data-sonner-toast][data-styled='true'] + [data-close-button][data-close-button-position='right'] { + left: 0; + right: unset; + transform: translate(-35%, -35%); +} + [data-sonner-toast][data-styled='true'] [data-disabled='true'] { cursor: not-allowed; } diff --git a/src/types.ts b/src/types.ts index e74740d..cb5180f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -70,6 +70,7 @@ export interface ToastT { richColors?: boolean; invert?: boolean; closeButton?: boolean; + closeButtonPosition?: CloseButtonPosition; dismissible?: boolean; description?: (() => React.ReactNode) | React.ReactNode; duration?: number; @@ -95,6 +96,7 @@ export function isAction(action: Action | React.ReactNode): action is Action { } export type Position = 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'top-center' | 'bottom-center'; +export type CloseButtonPosition = 'left' | 'right'; export interface HeightT { height: number; toastId: number | string; @@ -104,6 +106,7 @@ export interface HeightT { interface ToastOptions { className?: string; closeButton?: boolean; + closeButtonPosition?: CloseButtonPosition; descriptionClassName?: string; style?: React.CSSProperties; cancelButtonStyle?: React.CSSProperties; @@ -137,6 +140,7 @@ export interface ToasterProps { gap?: number; visibleToasts?: number; closeButton?: boolean; + closeButtonPosition?: CloseButtonPosition; toastOptions?: ToastOptions; className?: string; style?: React.CSSProperties; @@ -166,6 +170,7 @@ export interface ToastProps { visibleToasts: number; expandByDefault: boolean; closeButton: boolean; + closeButtonPosition?: CloseButtonPosition; interacting: boolean; style?: React.CSSProperties; cancelButtonStyle?: React.CSSProperties; diff --git a/test/src/app/page.tsx b/test/src/app/page.tsx index ef50965..cc3f0b9 100644 --- a/test/src/app/page.tsx +++ b/test/src/app/page.tsx @@ -369,6 +369,7 @@ export default function Home({ searchParams }: any) { { await expect(page.getByTestId('promise-test-toast')).toHaveText('Loading...'); await expect(page.getByTestId('promise-test-toast')).toHaveText('Loaded'); }); + + test('close button is positioned on the left by default', async ({ page }) => { + await page.getByTestId('close-button').click(); + const closeButton = page.locator('[data-close-button]'); + await expect(closeButton).toHaveAttribute('data-close-button-position', 'left'); + }); + + test('close button is positioned on the right when closeButtonPosition is right', async ({ page }) => { + await page.goto('/?closeButtonPosition=right'); + await page.getByTestId('close-button').click(); + const closeButton = page.locator('[data-close-button]'); + await expect(closeButton).toHaveAttribute('data-close-button-position', 'right'); + }); });