Start typing to search packages!
notificationkit
By @biotoxin495
Roblox
MirroredNotificationKit - 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.newexpectsPlayers.LocalPlayerunless you supplyRootorParentexplicitly. - 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, andAnnouncementvariants; 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 generatedScreenGui(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
Safe for commercial use
Automated license review — not legal advice.
