Forest Logo
search
package_2

notificationkit

By @biotoxin495

Roblox

Mirrored

NotificationKit - a typed, framework-independent notification engine for Roblox

NotificationKit is a notification orchestration system: it decides when notifications appear, where they appear, how multiple notifications interact, and how their UI is constructed, while remaining independent of Fusion, React, Knit, or any other framework.

One lifecycle engine powers everything from a one-line toast to a fully custom, structurally-defined notification card:

notifications:Toast("Saved successfully")
local handle = notifications:Show({
	Channel = "Downloads",
	Variant = "Progress",
	Title = "Loading inventory",
	Text = "Fetching item data...",
	Progress = 0,
	AutoDismiss = false,
})

handle:Update({
	Progress = 0.65,
	Text = "Loading item thumbnails...",
})

Both calls share the same queue, channel, timing, rendering, and cleanup systems.

Quick example

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local NotificationKit = require(ReplicatedStorage.NotificationKit)

local notifications = NotificationKit.new({
	Parent = Players.LocalPlayer.PlayerGui,
})

notifications:Toast("Checkpoint reached")

🚀 Features

Simple helpers

notifications:Toast("Saved successfully")
notifications:Info("New area discovered")
notifications:Success("Purchase complete")
notifications:Warning("Inventory nearly full")
notifications:Error("Failed to load profile")

These are thin wrappers around Show - Success/Warning/Error apply a semantic accent color, Info tags the notification's Metadata.Semantic.

Structured notifications

local handle = notifications:Show({
	Variant = "Toast",
	Text = "You received 250 coins",
	Icon = "rbxassetid://123",
	IconColor = Color3.fromRGB(255, 220, 80),
	Duration = 4,
	Priority = NotificationKit.Priority.High,
})

Action notifications

notifications:Show({
	Variant = "Action",
	Title = "Daily reward",
	Text = "Your daily reward is ready to claim.",
	Icon = "rbxassetid://123",
	Action = {
		Text = "Claim",
		Style = "Primary",
		Callback = function()
			claimReward()
		end,
	},
	SecondaryAction = {
		Text = "Dismiss",
		Style = "Secondary",
	},
})

Action notifications default to a 10-second duration and always render a close button. If a persistent (AutoDismiss = false) action notification has no Action or SecondaryAction, debug mode warns that it has no exit path.

Progress notifications

local handle = notifications:Show({
	Variant = "Progress",
	Title = "Downloading assets",
	Progress = 0,
})

handle:Update({ Progress = 0.4, Text = "Downloading textures..." })
handle:Update({ Progress = 1, Text = "Complete" })
handle:Dismiss("Programmatic")

Progress notifications default to AutoDismiss = false and auto-generate a "NN%" progress label unless ProgressText is supplied. Progress is always clamped to 0..1.

Announcements

notifications:Show({
	Variant = "Announcement",
	Title = "Round starting",
	Text = "The next round begins in 10 seconds.",
})

Notifications with Variant = "Announcement" are routed to a dedicated Announcements channel (top-center, stacked, up to 2 visible) unless a Channel is explicitly given.

Channels

Channels are independent notification lanes so gameplay toasts, prompts, and system warnings never block each other.

notifications:CreateChannel("Prompts", {
	Position = "TopCenter",
	DefaultPolicy = "Queue",
	MaxVisible = 1,
})

notifications:CreateChannel("System", {
	Position = "BottomRight",
	MaxVisible = 2,
})

notifications:GetChannel("Prompts"):Show({ Text = "Server restarting soon" })

The Default channel is created automatically. Position presets: TopLeft, TopCenter, TopRight, Center, BottomLeft, BottomCenter, BottomRight.

Delivery policies

Policy = "Queue"     -- wait for room in the channel
Policy = "Stack"     -- display alongside other active notifications (channel default)
Policy = "Replace"   -- dismiss a matching or active notification, then show this one
Policy = "Drop"      -- discard this notification if the channel is busy
Policy = "Coalesce"  -- merge into an existing notification sharing its Key
notifications:Show({
	Key = "CoinsReceived",
	Policy = "Coalesce",
	Text = "+5 coins",
	Metadata = { Amount = 5 },
	Merge = function(existing, incoming)
		local total = existing.Metadata.Amount + incoming.Metadata.Amount
		existing.Metadata.Amount = total
		existing.Text = `+{total} coins`
		return existing
	end,
})

Higher Priority notifications are shown first; equal-priority notifications preserve FIFO order.

Priority = NotificationKit.Priority.Low      -- -100
Priority = NotificationKit.Priority.Normal   -- 0
Priority = NotificationKit.Priority.High     -- 100
Priority = NotificationKit.Priority.Critical -- 1000

Themes

notifications:RegisterTheme("Neon", {
	BackgroundColor = Color3.fromRGB(8, 8, 12),
	AccentColor = Color3.fromRGB(80, 255, 230),
	StrokeColor = Color3.fromRGB(80, 255, 230),
	CornerRadius = UDim.new(0, 8),
})

notifications:Show({ Theme = "Neon", Text = "Power restored" })

Themes can extend another registered theme with Extends. Built-in themes: Default, Minimal, and HighContrast. Theme resolution order is: controller default theme → channel default theme → notification's Theme field.

Custom UI construction

Every default notification is built with Instance.new - no .rbxm template is required. Three levels of customization are supported:

Declarative instance trees, built per node with semantic Roles (Icon, Title, Text, ActionButton, SecondaryActionButton, CloseButton, ProgressFill, ProgressText, VisualContainer, ...):

notifications:RegisterBuildConfig("RewardCard", {
	Tree = {
		ClassName = "Frame",
		Name = "Root",
		Role = "Root",
		Children = {
			{ ClassName = "UICorner", Properties = { CornerRadius = UDim.new(0, 16) } },
			{ ClassName = "TextLabel", Name = "RewardTitle", Role = "Title" },
			{
				ClassName = "TextButton",
				Name = "ClaimButton",
				Role = "ActionButton",
				When = function(data)
					return data.Action ~= nil
				end,
			},
		},
	},
})

notifications:Show({ BuildConfig = "RewardCard", Title = "Reward!", Action = { Text = "Claim" } })

Layout-only configuration, changing padding, spacing, and minimum height without touching structure:

notifications:RegisterBuildConfig("CompactToast", {
	Layout = { Padding = 10, Spacing = 8, MinimumHeight = 42 },
})

Fully programmatic builders, for complete control:

notifications:RegisterBuilder("MyBuilder", function(context)
	local root = context.Create("Frame", { BackgroundColor3 = context.Theme.BackgroundColor })
	local title = context.Create("TextLabel", { Parent = root })
	return { Root = root, Elements = { Title = title } }
end)

Renderers themselves are swappable per notification, per channel, or globally via RegisterRenderer.

Sounds

local notifications = NotificationKit.new({
	Sound = { Enabled = true, Default = "rbxassetid://123", Volume = 0.5 },
})

notifications:Show({ Text = "Purchase complete", Sound = "rbxassetid://456" })

Sounds play once the notification becomes active, are parented to SoundService, and are destroyed automatically when playback ends or the notification is cleaned up. Set SoundEnabled = false per notification, or Sound.Enabled = false on the controller, to suppress playback.

Optional history

local notifications = NotificationKit.new({
	History = { Enabled = true, MaxEntries = 100 },
})

notifications:GetActive("Rewards")
notifications:GetHistory("Rewards", 20)

History is disabled by default. Only notifications that were actually shown are retained, newest first, bounded by MaxEntries.

Live handles

Every Show call returns a handle for inspecting and controlling that notification:

local handle = notifications:Show({ Text = "Uploading..." })

handle.Shown:Connect(function() print("visible now") end)
handle.Dismissed:Connect(function(reason) print("dismissed:", reason) end)

handle:Update({ Text = "Almost done..." })
handle:GetState()      -- "Created" | "Queued" | "Entering" | "Active" | "Exiting" | "Dismissed" | "Dropped"
handle:AwaitDismissed() -- yields until dismissed, returns the reason
handle:Dismiss("Programmatic")

Server-triggered notifications

-- Server
remote:FireClient(player, {
	Variant = "Action",
	Title = "Trade request",
	Text = "PlayerName wants to trade.",
	Action = { Id = "AcceptTrade", Text = "Accept" },
})

-- Client
notifications:BindRemote(remote, function(payload)
	return payload.Variant ~= "Announcement" -- optional payload validation
end)

BindRemote rewires any Action/SecondaryAction on the incoming payload to fire the same RemoteEvent back to the server with the notification's ID and action ID, so the server can independently validate the underlying game action.

Safety and cleanup

All user callbacks (Action.Callback, OnShow, OnUpdate, OnDismiss, Merge, declarative When/Transform, custom builders) run through a protected xpcall runner - a callback error is warned and never breaks queue processing, timeouts, or the next notification's delivery. Every notification owns a cleanup container that tracks connections, tweens, threads, sounds, and cloned visuals, guaranteeing no leaks on dismissal or controller destruction. Enable Strict = true to turn consumer mistakes (unknown policies, invalid durations) into hard errors during development; enable Debug = true for descriptive warnings instead.

Accessibility and responsiveness

Notifications pause their auto-dismiss timer on hover and gamepad/keyboard focus by default (PauseOnHover, PauseOnFocus), and can optionally pause while the game window is unfocused (PauseWhenGameUnfocused). ReducedMotion = true shortens and simplifies entrance/exit animations. Text uses UITextSizeConstraint and wraps within a max-width container instead of relying on unconstrained TextScaled.

📖 Basic usage

NotificationKit is intended for client-side UI and must be required from a LocalScript.

local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

local NotificationKit = require(ReplicatedStorage.NotificationKit)

local notifications = NotificationKit.new({
	Parent = Players.LocalPlayer.PlayerGui,
	DisplayOrder = 100,
	Debug = game:GetService("RunService"):IsStudio(),
})

If Parent is a LayerCollector or GuiObject, it's used directly as the root; otherwise NotificationKit creates and owns its own ScreenGui. Pass Root instead of Parent to reuse an existing ScreenGui/GuiObject you manage yourself.

⚙️ API

Controller

NotificationKit.new(config?)

Creates a controller and its Default channel. See Controller configuration below.

notifications:Show(data)                          -- show a structured notification, returns a handle
notifications:Toast(text, options?)
notifications:Info(text, options?)
notifications:Success(text, options?)
notifications:Warning(text, options?)
notifications:Error(text, options?)

notifications:CreateChannel(name, config?)
notifications:GetChannel(name)
notifications:DestroyChannel(name)

notifications:RegisterTheme(name, theme)
notifications:RegisterRenderer(name, renderer)
notifications:RegisterBuilder(name, builder)
notifications:RegisterBuildConfig(name, config)

notifications:GetHandle(id)
notifications:GetActive(channel?)
notifications:GetQueued(channel?)
notifications:GetHistory(channel?, limit?)

notifications:Dismiss(id, reason?)
notifications:DismissByKey(key, reason?)
notifications:Clear(channel?)
notifications:Pause(channel?)
notifications:Resume(channel?)

notifications:BindRemote(remote, validator?)       -- connect a RemoteEvent to Show()
notifications:Destroy()                            -- tears down every channel, notification, and the GUI root

Channel

local channel = notifications:GetChannel("Prompts")

channel:Show(data)
channel:GetActive()
channel:GetQueued()
channel:GetHistory(limit?)
channel:GetCount()
channel:Dismiss(id, reason?)
channel:DismissByKey(key, reason?)
channel:Clear(reason?)
channel:Pause()
channel:Resume()
channel:SetConfig(patch)
channel:Destroy()

Handle

handle:Update(patch)      -- partial patch; returns false if already dismissed
handle:Dismiss(reason?)
handle:IsActive()
handle:IsQueued()
handle:IsDismissed()
handle:GetState()
handle:GetData()
handle:AwaitShown()
handle:AwaitDismissed()
handle.Shown           -- fired when the notification becomes Active
handle.Updated         -- fired on every Update(), with (handle, patch)
handle.Dismissed       -- fired with (reason, handle)
handle.ActionTriggered -- fired with (actionId, handle) when Action/SecondaryAction activates

Complete options reference

Any field you omit falls back to the channel or controller default.

{
	Id = nil,               -- auto-generated if omitted
	Key = nil,               -- required for Coalesce, used by DismissByKey
	Channel = "Default",
	Variant = "Toast",       -- "Toast" | "Action" | "Progress" | "Announcement" | custom string

	Title = nil,
	Text = nil,
	RichText = false,

	Icon = nil,
	IconColor = nil,
	IconTransparency = 0,

	Visual = nil,            -- a GuiObject to embed
	CloneVisual = true,

	Action = nil,            -- { Text, Style, Icon, Callback, AutoDismiss }
	SecondaryAction = nil,
	OnActivated = nil,       -- fires when the notification body itself is clicked

	Progress = nil,          -- 0..1
	ProgressText = nil,

	Priority = 0,
	Policy = nil,            -- channel's DefaultPolicy, usually "Stack"
	Duration = 4,            -- 10 for Action
	AutoDismiss = true,      -- false for Progress

	Theme = nil,             -- theme name or inline ThemeConfig
	Renderer = nil,          -- renderer name or NotificationRenderer table
	BuildConfig = nil,       -- build config name or inline NotificationBuildConfig

	Sound = nil,
	SoundEnabled = true,
	SoundVolume = nil,       -- clamped 0..10, controller default 0.5
	SoundPlaybackSpeed = nil,-- clamped 0.05..4, controller default 1

	Metadata = nil,
	Merge = nil,             -- (existing, incoming) -> NotificationData, for Coalesce

	OnShow = nil,
	OnUpdate = nil,
	OnDismiss = nil,         -- (reason, handle)
}

Controller configuration

NotificationKit.new({
	Parent = playerGui,           -- or Root = existingScreenGui
	Name = "NotificationKit",
	DisplayOrder = 100,
	DefaultTheme = "Default",
	Debug = false,
	Strict = false,
	ReducedMotion = false,
	PauseOnHover = true,
	PauseOnFocus = true,
	PauseWhenGameUnfocused = false,
	ReadableDuration = { Enabled = false, BaseSeconds = 2, CharactersPerSecond = 18, MaximumSeconds = 12 },
	History = { Enabled = false, MaxEntries = 100 },
	Sound = { Enabled = true, Default = nil, Volume = 0.5, PlaybackSpeed = 1 },
})

Channel configuration

notifications:CreateChannel("Gameplay", {
	Position = "TopCenter",        -- TopLeft | TopCenter | TopRight | Center | BottomLeft | BottomCenter | BottomRight
	Parent = nil,                  -- supply a GuiObject to bypass the built-in container/positioning entirely
	LayoutDirection = "Vertical",  -- "Vertical" | "Horizontal"
	Spacing = 8,
	MaxVisible = 5,
	MaxQueued = 50,
	DefaultVariant = "Toast",
	DefaultPolicy = "Stack",
	DefaultDuration = nil,
	DefaultTheme = nil,
	DefaultRenderer = nil,
	OverflowPolicy = "DropNewest", -- "DropNewest" | "DropOldest" | "ReplaceOldest", applied when the queue is full
})

Behavior

A Default channel exists as soon as the controller is created; an Announcements channel is created automatically the first time an Announcement-variant notification is shown without an explicit Channel.

Within a channel, queued notifications are sorted by priority (descending) then by arrival order (ascending) whenever the queue is processed. Replace dismisses a matching (Key) or the oldest active notification before activating the new one; Drop discards the incoming notification if the channel has no room; Coalesce merges into an existing notification sharing its Key, using the optional Merge callback or otherwise replacing its data outright.

Auto-dismiss timers start once a notification finishes entering, pause while hovered or focused (or while the channel/controller is explicitly paused), and resume from the remaining duration rather than restarting. Dismissing an entry always cancels its timer, disables its interactions, fires OnDismiss, plays the exit animation, and only then destroys its rendered UI and reprocesses the channel's queue.

📝 Notes

  • NotificationKit is intended for client-side UI and must be required from a LocalScript; NotificationKit.new expects Players.LocalPlayer unless you supply Root or Parent explicitly.
  • The module ships as a single, dependency-free, fully typed (--!strict) ModuleScript - no external Signal or Janitor package is required.
  • Built-in renderers cover the Toast, Action, Progress, and Announcement variants; anything else falls back to the same default builder unless you register a custom renderer for that variant name.
  • notifications:Destroy() tears down every channel, active and queued notification, registered signal, and the generated ScreenGui (if NotificationKit created it).

🛠️ Installation

Manual installation

Place the NotificationKit ModuleScript somewhere accessible to your client scripts.

Recommended structure:

ReplicatedStorage
└── NotificationKit

Then require it with:

local ReplicatedStorage = game:GetService("ReplicatedStorage")

local NotificationKit = require(ReplicatedStorage.NotificationKit)

License

This project is available under the license included in the repository.

made with ❤️ by biotoxin495

Package Details

Install command (Click to copy)


Version

1.0.1

License

MIT

check_circle

Safe for commercial use

Automated license review — not legal advice.