Skip to main content

top-layer

Top Layer is a library for working with pop-up windows in a separate isolated layer without application rerenders.

The library allows you to manage dialogs and toasts in this layer. This layer is supported by all modern browsers. It includes dialog API and toast API.

However, the toast API does not work correctly on top of the dialog API, so toasts in the library are implemented in a different way (read more about toasts).

Installation 

npm i top-layer

Configuration 

For the package to work, the application must be wrapped in a provider

import { TopLayerProvider } from "top-layer";

import { ShareDialog } from "@src/components/share-dialog";
import { FullViewDialog } from "@src/components/full-view-dialog";
import { Toast } from "@src/components/toast";

const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<TopLayerProvider
toast={Toast}
dialogs={[
{ dialog: ShareDialog, id: "share" },
{ dialog: FullViewDialog, id: "full-view" },
]}
>
{children}
</TopLayerProvider>
);

export default RootLayout;

Note

TopLayerProvider never causes rerenders, so it is recommended to install it over the entire application.

Usage 

As already mentioned, the library creates an interface for working with dialogs and toasts. You can use them together or separately, in the desired order and in the desired condition.

Dialogs 

Since the library is not a UI library, but only creates the necessary API, you need to create dialogs.

Example of a basic dialog:

tsx [tailwind]
tsx [css]
css
"use client";

import { useState } from "react";
import { useDialogAction } from "top-layer/dialog/hooks";
import { Dialog } from "top-layer/dialog";
import { ToastAction } from "../toast-action";

import "./alert-dialog.scss";

export const AlertDialog: React.FC = () => {
const [state, setState] = useState<string | null>(null);
const { closeDialog } = useDialogAction("alert");

return (
<Dialog
className="items-center bg-transparent w-full h-full p-16 m-0 max-w-none max-h-none border-0 open:grid"
id="alert"
blockOverflow
onOpen={setState}
onClose={() => setState(null)}
>
<div
className="fixed left-0 top-0 w-full h-full bg-slate-950/20 backdrop-blur-[2px]"
onClick={closeDialog}
/>
<div className="relative m-auto p-16 bg-slate-900 rounded-2xl w-full max-w-5xl z-10">
<p>{state}</p>
<ToastAction type="neutral" message="Root Toast" title="Show Toast in Root" />
<ToastAction type="neutral" message="Dialog Toast" layers={['alert']} title="Show Toast in Dialog" />
<ToastAction type="neutral" message="Both Toast" layers={['root', 'alert']} title="Show Toast in Both" />
</div>
</Dialog>
);
};
"use client";

import { useState } from "react";
import { useDialogAction } from "top-layer/dialog/hooks";
import { Dialog } from "top-layer/dialog";
import { ToastAction } from "../toast-action";

import "./alert-dialog.scss";

export const AlertDialog: React.FC = () => {
const [state, setState] = useState<string | null>(null);
const { closeDialog } = useDialogAction("alert");

return (
<Dialog
className="alert-dialog"
id="alert"
blockOverflow
onOpen={setState}
onClose={() => setState(null)}
>
<div
className="alert-dialog-backdrop"
onClick={closeDialog}
/>
<div className="alert-dialog-content">
<p>{state}</p>
<ToastAction type="neutral" message="Root Toast" title="Show Toast in Root" />
<ToastAction type="neutral" message="Dialog Toast" layers={['alert']} title="Show Toast in Dialog" />
<ToastAction type="neutral" message="Both Toast" layers={['root', 'alert']} title="Show Toast in Both" />
</div>
</Dialog>
);
};
.alert-dialog {
align-items: center;
background-color: transparent;
width: 100%;
height: 100%;
padding: 16px;
margin: 0;
max-width: none;
max-height: none;
border: 0;

&[open] {
display: grid;
}
}

.alert-dialog-backdrop {
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgba(21, 21, 21, 0.2);
backdrop-filter: blur(2px);
}

.alert-dialog-content {
margin: auto;
position: relative;
padding: 16px;
background-color: #151515;
border-radius: 24px;
width: 100%;
max-width: 560px;
z-index: 10;
}

Where Dialog is the component responsible for all the logic.

Then you can open the dialog anywhere via the appropriate hook:

import { useDialogs } from "top-layer/dialog";
// ...
const { openDialog } = useDialogs();
// ...
<button onClick={() => openDialog("alert", "Base Alert Dialog")}>
Open Alert Dialog
</button>;

The first argument must be the id of the dialog, and the second - the data that will be passed to the dialog handlers (Read more in the section useDialogs)

Note

It is recommended to open and close dialogs through special hooks. However, in some cases you may need to use them outside the React runtime (for example inside redux saga). For these purposes, you can use the standalone methods openDialog and closeDialog.

Notifications 

For toasts to work in TopLayerProvider you need to pass your Toast component.

An example of such a Toast component:

tsx [tailwind]
tsx [css]
css
"use client";

import { useEffect } from "react";

import "./toast.scss";

export interface ToastProps {
message: string;
type: keyof typeof TYPES;
closeHandler: () => void;
}

const TYPES = {
success: "bg-green-500",
warning: "bg-yellow-600",
error: "bg-red-500",
neutral: "bg-neutral-500",
};

export const Toast: React.FC<ToastProps> = ({
message,
type = "neutral",
closeHandler,
}) => {
useEffect(() => {
const timer = setTimeout(closeHandler, 3000);

return () => clearTimeout(timer);
}, [closeHandler]);

return (
<div
className={cn(
"fixed bottom-16 left-1/2 -translateX-1/2 py-8 px-16 rounded-md flex items-center justify-center z-100 w-3xl max-w-full",
TYPES[type]
)}
onClick={closeHandler}
>
{message}
</div>
)
};
"use client";

import { useEffect } from "react";

import "./toast.scss";

export interface ToastProps {
message: string;
type: keyof typeof TYPES;
closeHandler: () => void;
}

const TYPES = {
success: "toast_success",
warning: "toast_warning",
error: "toast_error",
neutral: "toast_neutral",
};

export const Toast: React.FC<ToastProps> = ({
message,
type = "neutral",
closeHandler,
}) => {
useEffect(() => {
const timer = setTimeout(closeHandler, 3000);

return () => clearTimeout(timer);
}, [closeHandler]);

return (
<div
className={`toast ${TYPES[type]}`}
onClick={closeHandler}
>
{message}
</div>
)
};
.toast {
position: fixed;
bottom: 16px;
left: 50%;
transform: translateX(-50%);
padding: 8px 16px;
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
min-width: 300px;
max-width: 100%;
text-align: center;
}

.toast_success {
background-color: #008000;
}

.toast_warning {
background-color: #ffa500;
}

.toast_error {
background-color: #ff0000;
}

.toast_neutral {
background-color: #808080;
}

Show toast

import { useToasts } from "top-layer/toaster";

// ...
const { showToast } = useToasts();
// ...
showToast("data-loader", {
message: "Success Test",
type: "success",
}, ['root']);

Note

The data passed when calling showToast will be passed as props to the component. Read more in the useToasts section.

Note

It is recommended to open and close notifications through special hooks. However, in some cases you may need to use them outside the React runtime (for example inside redux saga). For these purposes, you can use the standalone methods showToast and hideToast.

API 

TopLayerProvider 

Main package provider. It must be added above the part of the application where you plan to use dialogs and toasts.

Tip

TopLayerProvider never causes rerenders, so it is recommended to install it over the entire application.

import { TopLayerProvider } from "top-layer";

import { ShareDialog } from "@src/components/share-dialog";
import { FullViewDialog } from "@src/components/full-view-dialog";
import { Toast } from "@src/components/toast";

const RootLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<TopLayerProvider
toast={Toast}
dialogs={[
{ dialog: ShareDialog, id: "share" },
{ dialog: FullViewDialog, id: "full-view" },
]}
>
{children}
</TopLayerProvider>
);

export default RootLayout;

Props

toast - Component that will be used for toasts (more details in the Notifications section).

dialogs - Array of dialogs with keys:

Dialog 

Dialogue wrapper. Instead of the dialog html element, you need to use a component from the Dialog package. See an example of use in the Dialogs section.

Tip

The component will configure all necessary connections and correct operation of toasts, so it is recommended to add dialogs only through it

Props

The component inherits all props from the dialog element

blockOverflow - block page scrolling when opening a dialog;

onOpen - action when opening a window. The argument will contain the data passed from the hook (more details in sections useDialogs and useDialogAction);

onClose - action when closing the dialog box.

useDialogs 

Universal hook for working with multiple dialogs

import { useDialogs } from "top-layer/dialog";
// ...
const { openDialog, closeDialog } = useDialogs();
// ...
<button onClick={() => openDialog("alert", "Some Alert Message")}>
Show Alert
</button>
// ...
<button onClick={() => closeDialog("modal")}>
Close Modal
</button>

Returns

openDialog - function for opening a dialog box. Arguments:

closeDialog - function for closing a dialog box. Arguments:

useDialogAction 

Works the same as useDialogs, but is intended for a single window

import { useDialogAction } from "top-layer/dialog";
// ...
const { openDialog, closeDialog } = useDialogAction('modal');
// ...
<button onClick={() => openDialog({ title: "Some modal title", description: "Lorem Ipsum Sit Amet" })}>
Show Modal
</button>
// ...
<button onClick={closeDialog}>
Close Modal
</button>

Arguments

id - dialogue id.

Returns

openDialog - function for opening a dialog box. Arguments:

closeDialog - function for closing a dialog box.

openDialog 

Works the same as openDialog returned from useDialogs

import { openDialog } from "top-layer/dialog";
// ...
<button onClick={() => openDialog("alert", "Some Alert Message")}>
Show Alert
</button>

Arguments

closeDialog 

Works the same as closeDialog returned from useDialogs

import { closeDialog } from "top-layer/dialog";
// ...
<button onClick={() => closeDialog("modal")}>
Close Modal
</button>

Arguments

useToasts 

Universal hook for working with multiple toasts

import { useToasts } from "top-layer/toaster";
// ...
const { showToast, hideToast } = useToasts();
// ...
<button onClick={() => showToast("invalid-arg", { message: "Invalid argument", type: 'error' })}>
Show Toast
</button>
// ...
<button onClick={() => hideToast("invalid-arg")}>
Hide Toast
</button>

Returns

showToast - function to show a toast. Arguments:

Note

Each dialog is a separate layer, you can. If you do not want to show a toast on top of dialogs, use root, if only on specific dialogs, pass an array of id of these dialogs.

hideToast - function for hiding toasts. Arguments:

useToastAction 

Hook for working with a specific toast

import { useToastAction } from "top-layer/toaster";
// ...
const { showToast, hideToast } = useToastAction("invalid-arg");
// ...
<button onClick={() => showToast({ message: "Invalid argument", type: 'error' })}>
Show Toast
</button>
// ...
<button onClick={hideToast}>
Hide Toast
</button>

Returns

showToast - function to show a toast. Arguments:

Note

Each dialog is a separate layer, you can. If you do not want to show a toast on top of dialogs, use root, if only on specific dialogs, pass an array of id of these dialogs.

hideToast - function for hiding toasts.

showToast 

Works the same as showToast returned from useToasts

import { showToast } from "top-layer/toaster";
// ...
<button onClick={() => showToast("invalid-arg", { message: "Invalid argument", type: 'error' })}>
Show Toast
</button>

Arguments

hideToast 

Works the same as hideToast returned from useToasts

import { hideToast } from "top-layer/toaster";
// ...
<button onClick={() => hideToast("invalid-arg")}>
Hide Toast
</button>

Arguments

Additional 

Please consider giving a star if you like it, this motivates the author to continue working on this and other solutions ❤️

Create issues with wishes, ideas, difficulties, etc. All of them will definitely be considered and thought over.

License 

MIT 

Last modified on
Previous
Edge A/B Tests
Next
Cache Adapter
Return to navigation