Skip to content

Commit

Permalink
Easier classname customization (#232)
Browse files Browse the repository at this point in the history
* Create classNames prop

* Add styling section

* Make title class optional

* Polish

* Add todo
  • Loading branch information
emilkowalski authored Nov 20, 2023
1 parent 84745a2 commit e78eb41
Show file tree
Hide file tree
Showing 4 changed files with 147 additions and 6 deletions.
42 changes: 36 additions & 6 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import ReactDOM from 'react-dom';

import './styles.css';
import { getAsset, Loader } from './assets';
import type { HeightT, Position, ToastT, ToastToDismiss, ExternalToast, ToasterProps } from './types';
import type { HeightT, Position, ToastT, ToastToDismiss, ExternalToast, ToasterProps, ToastClassnames } from './types';
import { ToastState, toast } from './state';

// Visible toasts amount
Expand Down Expand Up @@ -50,6 +50,11 @@ interface ToastProps {
unstyled?: boolean;
descriptionClassName?: string;
loadingIcon?: React.ReactNode;
classNames?: ToastClassnames;
}

function cn(...classes: (string | undefined)[]) {
return classes.filter(Boolean).join(' ');
}

const Toast = (props: ToastProps) => {
Expand All @@ -75,6 +80,7 @@ const Toast = (props: ToastProps) => {
gap = GAP,
loadingIcon: loadingIconProp,
expandByDefault,
classNames,
} = props;
const [mounted, setMounted] = React.useState(false);
const [removed, setRemoved] = React.useState(false);
Expand All @@ -90,7 +96,6 @@ const Toast = (props: ToastProps) => {
const dismissible = toast.dismissible !== false;
const toastClassname = toast.className || '';
const toastDescriptionClassname = toast.descriptionClassName || '';

// Height index is used to calculate the offset as it gets updated before the toast array, which means we can calculate the new layout faster.
const heightIndex = React.useMemo(
() => heights.findIndex((height) => height.toastId === toast.id) || 0,
Expand Down Expand Up @@ -120,6 +125,7 @@ const Toast = (props: ToastProps) => {
const disabled = toastType === 'loading';

offset.current = React.useMemo(() => heightIndex * gap + toastsHeightBefore, [heightIndex, toastsHeightBefore]);
console.log(classNames);

React.useEffect(() => {
// Trigger enter animation without using CSS animation
Expand Down Expand Up @@ -215,7 +221,10 @@ const Toast = (props: ToastProps) => {
function getLoadingIcon() {
if (loadingIconProp) {
return (
<div className="loader" data-visible={toastType === 'loading'}>
<div
className={cn('loader', classNames?.toast, toast?.classNames?.toast)}
data-visible={toastType === 'loading'}
>
{loadingIconProp}
</div>
);
Expand All @@ -230,7 +239,14 @@ const Toast = (props: ToastProps) => {
role="status"
tabIndex={0}
ref={toastRef}
className={className + ' ' + toastClassname}
className={cn(
className,
toastClassname,
classNames?.toast,
toast?.classNames?.toast,
classNames?.[toastType],
toast?.classNames?.[toastType],
)}
data-sonner-toast=""
data-styled={!Boolean(toast.jsx || toast.unstyled)}
data-mounted={mounted}
Expand Down Expand Up @@ -321,6 +337,7 @@ const Toast = (props: ToastProps) => {
toast.onDismiss?.(toast);
}
}
className={cn(classNames?.closeButton, toast?.classNames?.closeButton)}
>
<svg
xmlns="http://www.w3.org/2000/svg"
Expand Down Expand Up @@ -350,9 +367,19 @@ const Toast = (props: ToastProps) => {
) : null}

<div data-content="">
<div data-title="">{toast.title}</div>
<div data-title="" className={cn(classNames?.title, toast?.classNames?.title)}>
{toast.title}
</div>
{toast.description ? (
<div data-description="" className={descriptionClassName + toastDescriptionClassname}>
<div
data-description=""
className={cn(
descriptionClassName,
toastDescriptionClassname,
classNames?.description,
toast?.classNames?.description,
)}
>
{toast.description}
</div>
) : null}
Expand All @@ -369,6 +396,7 @@ const Toast = (props: ToastProps) => {
toast.cancel.onClick();
}
}}
className={cn(classNames?.cancelButton, toast?.classNames?.cancelButton)}
>
{toast.cancel.label}
</button>
Expand All @@ -382,6 +410,7 @@ const Toast = (props: ToastProps) => {
if (event.defaultPrevented) return;
deleteToast();
}}
className={cn(classNames?.actionButton, toast?.classNames?.actionButton)}
>
{toast.action.label}
</button>
Expand Down Expand Up @@ -630,6 +659,7 @@ const Toaster = (props: ToasterProps) => {
interacting={interacting}
position={position}
style={toastOptions?.style}
classNames={toastOptions?.classNames}
cancelButtonStyle={toastOptions?.cancelButtonStyle}
actionButtonStyle={toastOptions?.actionButtonStyle}
removeToast={removeToast}
Expand Down
17 changes: 17 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ export type PromiseData<ToastData = any> = ExternalToast & {
finally?: () => void | Promise<void>;
};

export interface ToastClassnames {
toast?: string;
title?: string;
description?: string;
loader?: string;
closeButton?: string;
cancelButton?: string;
actionButton?: string;
success?: string;
error?: string;
info?: string;
warning?: string;
}

// TODO: CLEANUP TYPES
export interface ToastT {
id: number | string;
title?: string | React.ReactNode;
Expand Down Expand Up @@ -39,6 +54,7 @@ export interface ToastT {
style?: React.CSSProperties;
unstyled?: boolean;
className?: string;
classNames?: ToastClassnames;
descriptionClassName?: string;
position?: Position;
}
Expand All @@ -57,6 +73,7 @@ interface ToastOptions {
actionButtonStyle?: React.CSSProperties;
duration?: number;
unstyled?: boolean;
classNames?: ToastClassnames;
}

export interface ToasterProps {
Expand Down
8 changes: 8 additions & 0 deletions website/src/pages/_meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,13 @@
"toaster": {
"title": "Toaster",
"href": "/toaster"
},
"-- GUIDES": {
"type": "separator",
"title": "Guides"
},
"styling": {
"title": "Styling",
"href": "/styling"
}
}
86 changes: 86 additions & 0 deletions website/src/pages/styling.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Styling

Styling can be done globally via `toastOptions`, this way every toast will have the same styling.

```jsx
<Toaster
toastOptions={{
style: {
background: 'red',
},
className: 'class',
}}
/>
```

You can also use the same props when calling `toast` to style a specific toast.

```jsx
toast('Hello World', {
style: {
background: 'red',
},
className: 'class',
});
```

## Tailwind CSS

The preferred way to style the toasts with tailwind is by using the `classNames` prop. Exclamation mark in front of the class is required to override the default styles.

```jsx
<Toaster
toastOptions={{
classNames: {
toast: '!bg-blue-400',
title: '!text-red-400',
description: '!text-red-400',
actionButton: '!bg-zinc-400',
cancelButton: '!bg-orange-400',
closeButton: '!bg-lime-400',
},
}}
/>
```

You can do the same when calling `toast()`.

```jsx
toast('Hello World', {
classNames: {
toast: '!bg-blue-400',
title: '!text-red-400 !text-2xl',
description: '!text-red-400',
actionButton: '!bg-zinc-400',
cancelButton: '!bg-orange-400',
closeButton: '!bg-lime-400',
},
});
```

Styling per toast type is also possible.

```jsx
<Toaster
toastOptions={{
classNames: {
error: '!bg-red-400',
success: '!text-green-400',
warning: '!text-yellow-400',
info: '!bg-blue-400',
},
}}
/>
```

## No styles

You can also disable default styles by going headless.

```jsx
toast.custom((t) => (
<div>
This is a custom component <button onClick={() => toast.dismiss(t)}>close</button>
</div>
));
```

1 comment on commit e78eb41

@vercel
Copy link

@vercel vercel bot commented on e78eb41 Nov 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.