Fed up with MouseEnter and MouseLeave not working? Here's a module for you!

Didn’t work for me. Could you be more specific in where to put each part of the script and module?

Make sure you require it first.

Then you gotta make the varible for the actuall events.

You would probably would want to use a local script and the the module script in replicated storage.

That should help.

Hi! I was wondering if anyone here has a solution to where it doesn’t keep detecting frames that aren’t visible on screen?

For example, the frame is still visible but the parent frames aren’t. Is there a way to detect or check if their parent frames are also visible?

1 Like

Any reason as to why this isnt working im so confused

local MouseOverModule = require(game.ReplicatedStorage.Packages.MouseOverModule)
local LeftIntHolder = game.StarterGui.MainUI.ScreenBorder.LeftInteractionHolder

local MouseEnter, MouseLeave = MouseOverModule.MouseEnterLeaveEvent(LeftIntHolder.MenuSideInteraction)

MouseEnter:Connect(function()
	LeftIntHolder.MenuSideInteraction:TweenSize(UDim2.new(0,75,0,75))
	print("mouse entered")
end)

MouseLeave:Connect(function()
	LeftIntHolder.MenuSideInteraction:TweenSize(UDim2.new(0,60,0,60))
	print("mouse left")
end)

it prints but does not tweensize

maybe because youre only changing the one in StarterGui (which is essentially the guis template when a player respawn) instead of the PlayerGui itself

1 Like
game:GetService("RunService").Heartbeat:Connect(function()
	--Check each UI object
	--All exit events fire before all enter events for ease of use, so check for mouse exit events here
	for _, Object in pairs(CurrentItems) do
		Object.MouseIsInFrame = IsInFrame(Object.UIObj)
		CheckMouseExited(Object)
	end

	--Now check if the mouse entered any frames
	for _, Object in pairs(CurrentItems) do
		if Object.UIObj.Visible == true then
			if Object.UIObj.Parent.Visible == true then
				CheckMouseEntered(Object)
			end
		end
	end
end)

This works, but I just realized if there’s another parent thats not visible it still detects your mouse. My solution to that was to add another if parent visible :face_with_diagonal_mouth:

If anyone is having the issue where MouseEnter fires even when the gui element isn’t visible, it’s as simple as returning false if the gui element isn’t visible.

local function IsInFrame(v)
	
	if not v.Visible then
		return false
	end

	local X = Mouse.X
	local Y = Mouse.Y

	if X>v.AbsolutePosition.X and Y>v.AbsolutePosition.Y and X<v.AbsolutePosition.X+v.AbsoluteSize.X and Y<v.AbsolutePosition.Y+v.AbsoluteSize.Y then
		return true
	else 
		return false
	end
end
1 Like

Sorry for the bump, but i tweaked the module to work without player:GetMouse() (and cleaned it up a bit.)

-- module by @madattak, tweaked by @KingBlueDash

local UserInputService = game:GetService("UserInputService")

local CurrentItems = {}

--Private functions
local function IsInFrame(v)
	
	if not v.Visible then
		return false
	end
	
	local X = UserInputService:GetMouseLocation().X
	
	local Y = UserInputService:GetMouseLocation().Y

	if X > v.AbsolutePosition.X and Y > v.AbsolutePosition.Y
		and
		X < v.AbsolutePosition.X + v.AbsoluteSize.X
		and
		Y < v.AbsolutePosition.Y + v.AbsoluteSize.Y
	then
		
		return true
		
	else 
		return false
	end
end

local function CheckMouseExited(Object)

	if not Object.MouseIsInFrame
		and Object.MouseWasIn then -- if mouse was previously over object, fires the leave event.
		
		Object.MouseWasIn = false
		
		Object.LeaveEvent:Fire()
		
	end
	
end


local function CheckMouseEntered(Object)
	
	if Object.MouseIsInFrame and not Object.MouseWasIn then -- does the oppisite of the
		-- above function.
		
		Object.MouseWasIn = true
		
		Object.EnteredEvent:Fire()
		
	end
	
end

game:GetService("RunService").PreRender:Connect(function()
	
	-- checks each UI object.
	
	-- all exit events fire before all enter events for ease of use
	-- so check for mouse exit events here.
	
	for _, Object in pairs(CurrentItems) do
		
		Object.MouseIsInFrame = IsInFrame(Object.UIObj)
		
		CheckMouseExited(Object)
		
	end

	--Now check if the mouse entered any frames
	
	for _, Object in pairs(CurrentItems) do
		CheckMouseEntered(Object)
	end
	
end)

--Public functions

local module = {}

function module.MouseEnterLeaveEvent(UIObj)
	
	if CurrentItems[UIObj] then
		return CurrentItems[UIObj].EnteredEvent.Event,CurrentItems[UIObj].LeaveEvent.Event
	end     

	local newObj = {}

	newObj.UIObj = UIObj

	local EnterEvent = Instance.new("BindableEvent")
	
	local LeaveEvent = Instance.new("BindableEvent")

	newObj.EnteredEvent = EnterEvent
	
	newObj.LeaveEvent = LeaveEvent
	
	newObj.MouseWasIn = false
	
	CurrentItems[UIObj] = newObj

	UIObj.Destroying:Connect(function()
		
		EnterEvent:Destroy()  

		LeaveEvent:Destroy()   

		CurrentItems[UIObj] = nil
		
	end)

	return EnterEvent.Event,LeaveEvent.Event
	
end

return module
1 Like

I’m a few years late but maybe someone else will find this useful. I had that same issue so I fixed it myself!

local Player = game.Players.LocalPlayer or game.Players:GetPropertyChangedSignal("LocalPlayer")

local Mouse = Player:GetMouse()

local CurrentItems = {}

--Private functions
local function IsInFrame(v)

	local X = Mouse.X
	local Y = Mouse.Y

	if X>v.AbsolutePosition.X and Y>v.AbsolutePosition.Y and X<v.AbsolutePosition.X+v.AbsoluteSize.X and Y<v.AbsolutePosition.Y+v.AbsoluteSize.Y then
		return true
	else 
		return false
	end
end

local function CheckMouseExited(Object)

	if not Object.MouseIsInFrame and Object.MouseWasIn then --Mouse was previously over object, fire leave event
		Object.MouseWasIn = false
		Object.LeaveEvent:Fire()
	end
end


local function CheckMouseEntered(Object)
	if Object.MouseIsInFrame and not Object.MouseWasIn then
		Object.MouseWasIn = true
		Object.EnteredEvent:Fire()
	end
end

game:GetService("RunService").Heartbeat:Connect(function()
	--Check each UI object
	--All exit events fire before all enter events for ease of use, so check for mouse exit events here
	for _, Object in pairs(CurrentItems) do
		Object.MouseIsInFrame = IsInFrame(Object.UIObj) and IsInFrame(Object.UIObj.Parent)
		CheckMouseExited(Object)
	end

	--Now check if the mouse entered any frames
	for _, Object in pairs(CurrentItems) do
		CheckMouseEntered(Object)
	end
end)

--Public functions

local module = {}

function module.MouseEnterLeaveEvent(UIObj)
	if CurrentItems[UIObj] then
		return CurrentItems[UIObj].EnteredEvent.Event,CurrentItems[UIObj].LeaveEvent.Event
	end     

	local newObj = {}

	newObj.UIObj = UIObj

	local EnterEvent = Instance.new("BindableEvent")
	local LeaveEvent = Instance.new("BindableEvent")

	newObj.EnteredEvent = EnterEvent
	newObj.LeaveEvent = LeaveEvent
	newObj.MouseWasIn = false
	CurrentItems[UIObj] = newObj

	UIObj.AncestryChanged:Connect(function()
		if not UIObj.Parent then
			--Assuming the object has been destroyed as we still dont have a .Destroyed event
			--If for some reason you parent your UI elements to nil after calling this, then parent it back again, mouse over will still have been disconnected.
			EnterEvent:Destroy()    
			LeaveEvent:Destroy()    
			CurrentItems[UIObj] = nil
		end
	end)

	return EnterEvent.Event,LeaveEvent.Event
end

return module

For debugging: All I did was also check if the mouse is within the bounds of the guiObject’s parent

1 Like

I made major changes to the module, improving performance and fixing some bugs. Some examples of fixed bugs are: when the being hovered GuiObject’s Visible is disabled, and the leave event doesn’t fire; when the GuiObject is destroyed, but the leave event is never fired; when you’re hovering a GuiObject while quickly moving your mouse and it doesn’t seem to update correctly; etc.

If anyone’s interested, here it is. I have been using it in my games for a while and I’ve had no issues so far.

local userInputService = game:GetService("UserInputService")
local runService = game:GetService("RunService")
local players = game:GetService("Players")

local player = players.LocalPlayer
local mouse = player:GetMouse()

export type Object = {
	MouseIsInFrame: boolean,
	MouseWasIn: boolean,
	LeaveEvent: BindableEvent,
	EnterEvent: BindableEvent
}

local currentItems: {[GuiObject]: Object} = {}

-- Private functions
local function CheckMouse(object: Object)
	if not object.MouseIsInFrame and object.MouseWasIn then
		object.MouseWasIn = false
		object.LeaveEvent:Fire()
	elseif object.MouseIsInFrame and not object.MouseWasIn then
		object.MouseWasIn = true
		object.EnterEvent:Fire()
	end
end

-- Main handler
local GetIsInFrame: (obj: GuiObject) -> () do
	local function IsInFrame(obj: GuiObject)
		if not obj.Visible then
			return false
		end

		local x, y = mouse.X, mouse.Y	
		local objPosition = obj.AbsolutePosition
		local objSize = obj.AbsoluteSize

		if 
			x > objPosition.X and y > objPosition.Y and
			x < objPosition.X + objSize.X and y < objPosition.Y + objSize.Y
		then
			return true
		end

		return false
	end
	
	GetIsInFrame = function(obj: GuiObject)
		return IsInFrame(obj) and (obj.Parent:IsA("GuiObject") and IsInFrame(obj.Parent))
	end
end

local function Update()
	for uIObject, obj in currentItems do
		if not uIObject.Parent then
			if obj.MouseWasIn then
				obj.MouseWasIn = false
				obj.LeaveEvent:Fire()
			end
			
			continue
		end
		
		obj.MouseIsInFrame = GetIsInFrame(uIObject)
		CheckMouse(obj)
	end
end

mouse.Move:Connect(Update)

--[[userInputService.InputChanged:Connect(function(inputObject, gameProcessedEvent)
	if inputObject.UserInputType == Enum.UserInputType.MouseMovement and not gameProcessedEvent then
		Update()
	end
end)]]

-- Public functions
local module = {}

function module:ConnectToEvents(uIObject: GuiObject, onEnter: (() -> ())?, onLeave: (() -> ())?)
	local currentObj = currentItems[uIObject]

	if currentObj then
		local enterEvent, leaveEvent = currentObj.EnterEvent.Event, currentObj.LeaveEvent.Event

		if onEnter then
			enterEvent.Event:Connect(onEnter)
		end

		if onLeave then
			leaveEvent.Event:Connect(onLeave)
		end

		return
	end     

	local enterEvent = Instance.new("BindableEvent")
	local leaveEvent = Instance.new("BindableEvent")
	local object = {
		EnterEvent = enterEvent,
		LeaveEvent = leaveEvent,
		MouseWasIn = false,
		MouseIsInFrame = false
	}
	
	uIObject.Destroying:Once(function()
		if object.MouseWasIn then
			object.MouseWasIn = false
			leaveEvent:Fire()
		end
		
		currentItems[uIObject] = nil
		enterEvent:Destroy()
		leaveEvent:Destroy()
	end)
	
	currentItems[uIObject] = object

	if onEnter then
		enterEvent.Event:Connect(onEnter)
	end

	if onLeave then
		leaveEvent.Event:Connect(onLeave)
	end
end

return module
1 Like

Hello guys! I found a bug that causes ‘mouseenter’ and ‘mouseleave’ to trigger even when the given GUI object isn’t visible to the player (ex. when it’s outside a ‘ScrollingFrame’), but I managed to fix it. I also optimized the script as best as I could, and after testing, it seems to work great. I’ll share my work with you all ;p

local Players: Players = game:GetService("Players")
local UIS: UserInputService = game:GetService("UserInputService")

local Player: Player = Players.LocalPlayer or Players:GetPropertyChangedSignal("LocalPlayer")
local Mouse: Mouse = Player:GetMouse()

local CurrentItems: {[Instance]: any} = {}

-- === Private helpers ===
local function IsInFrame(v: Instance, X: number, Y: number): boolean
	local inBounds = X > v.AbsolutePosition.X 
		and Y > v.AbsolutePosition.Y 
		and X < v.AbsolutePosition.X + v.AbsoluteSize.X 
		and Y < v.AbsolutePosition.Y + v.AbsoluteSize.Y

	if not inBounds then return end

	local scrollParent = v:FindFirstAncestorOfClass("ScrollingFrame")
	if scrollParent then
		local viewPos = scrollParent.AbsolutePosition
		local viewSize = scrollParent.AbsoluteSize
		if X < viewPos.X or X > viewPos.X + viewSize.X or Y < viewPos.Y or Y > viewPos.Y + viewSize.Y then
			return
		end
	end

	return true
end

local function ProcessHover()
	local X, Y = Mouse.X, Mouse.Y

	for _, Object in CurrentItems do
		local UIObj = Object.UIObj
		local prev = Object.MouseWasIn
		local now = IsInFrame(UIObj, X, Y)

		if now and not prev then
			Object.MouseWasIn = true
			Object.EnteredEvent:Fire()
			
		elseif not now and prev then
			Object.MouseWasIn = nil
			Object.LeaveEvent:Fire()
		end
	end
end

-- === Input listener ===
UIS.InputChanged:Connect(function(Input: InputObject)
	if Input.UserInputType ~= Enum.UserInputType.MouseMovement then return end
	ProcessHover()
end)

-- === Public API ===
local module = {}

function module.MouseEnterLeaveEvent(UIObj: Instance)
	if CurrentItems[UIObj] then
		return CurrentItems[UIObj].EnteredEvent.Event, CurrentItems[UIObj].LeaveEvent.Event
	end     

	local EnterEvent = Instance.new("BindableEvent")
	local LeaveEvent = Instance.new("BindableEvent")

	CurrentItems[UIObj] = {
		UIObj = UIObj,
		EnteredEvent = EnterEvent,
		LeaveEvent = LeaveEvent,
		MouseWasIn = nil,
	}

	UIObj.AncestryChanged:Connect(function()
		if UIObj.Parent then return end
		EnterEvent:Destroy()
		LeaveEvent:Destroy()
		CurrentItems[UIObj] = nil
	end)

	return EnterEvent.Event, LeaveEvent.Event
end

return module
1 Like

Going off what @Nightnessx & @instabologna123 created, I added onto it in my own way with these changes:

  • Utilizing Warp.
  • Defining AbsolutePosition and AbsoluteSize first for InBounds.
  • Instead of doing newObj.UIObj I used the UIObj that is from the lookup: newObj[UIObj].
  • Improved the types, now using --!strict.
  • From @Nightnessx, using an event-based listener instead of a ‘forever’ loop. RunService → UserInputService.
  • From @instabologna123, checking if the GuiObject is visible.
  • Disconnecting the connection created from AncestryChanged.
  • Added a new parameter to MouseEnterLeaveEvent, ProcessDeletion, in some cases you never want a GuiObject to be destroyed, so why ever create a connection to wait for it.
  • Changed Players:GetPropertyChangedSignal(‘LocalPlayer’) to Players:GetPropertyChangedSignal(‘LocalPlayer’):Wait(), I may be wrong but I believe this is the originally intended usage.
  • Renamed a lot of stuff to my liking. Purely optional, I just felt these were better names.

I didn’t extensively test this, so there may be issues I missed, so do let me know.

--!strict

local MouseOver = {}

-- // Services & Requires
local UserInputService: UserInputService = game:GetService('UserInputService')
local Players: Players = game:GetService('Players')
local Warp = require( game.ReplicatedStorage.Warp )

-- // Variables & Constants
local LocalPlayer: Player = Players.LocalPlayer or Players:GetPropertyChangedSignal('LocalPlayer'):Wait()
local Mouse: Mouse = LocalPlayer:GetMouse()

local RNG = Random.new()
local MouseMovement = Enum.UserInputType.MouseMovement

-- // Tables & Types
local Entries: { [GuiObject]: EntriesType } = {}

type SignalType = typeof( Warp.Signal('Type') )
type EntriesType = {
	EnteredEvent: SignalType,
	LeftEvent: SignalType,
	MouseWasIn: boolean,
}

-- // Functions
local function IsInFrame( GuiObject: GuiObject , X: number , Y: number ): boolean
	if not GuiObject.Visible then
		return false
	end
	
	local AbsolutePosition = GuiObject.AbsolutePosition
	local AbsoluteSize = GuiObject.AbsoluteSize
	local InBounds = X > AbsolutePosition.X
		and Y > AbsolutePosition.Y
		and X < AbsolutePosition.X + AbsoluteSize.X
		and Y < AbsolutePosition.Y + AbsoluteSize.Y
	if not InBounds then
		return false
	end

	local ScrollingFrame = GuiObject:FindFirstAncestorOfClass('ScrollingFrame')
	if ScrollingFrame and ScrollingFrame:IsA('ScrollingFrame') then
		local ViewAbsolutePosition = ScrollingFrame.AbsolutePosition
		local ViewAbsoluteSize = ScrollingFrame.AbsoluteSize
		if X < ViewAbsolutePosition.X or X > ViewAbsolutePosition.X + ViewAbsoluteSize.X or Y < ViewAbsolutePosition.Y or Y > ViewAbsolutePosition.Y + ViewAbsoluteSize.Y then
			return false
		end
	end
	
	return true
end

UserInputService.InputChanged:Connect(function( Input: InputObject )
	if Input.UserInputType ~= MouseMovement then
		return
	end
	local X , Y = Mouse.X, Mouse.Y
	for GuiObject , Entry in Entries do
		local WasIn , IsIn = Entry.MouseWasIn , IsInFrame( GuiObject , X , Y )
		if IsIn and not WasIn then
			Entry.MouseWasIn = true
			Entry.EnteredEvent:Fire()
		elseif not IsIn and WasIn then
			Entry.MouseWasIn = false
			Entry.LeftEvent:Fire()
		end
	end
end)

function MouseOver.MouseEnterLeaveEvent( GuiObject: GuiObject , ProcessDeletion: boolean? ): ( SignalType , SignalType )
	local ExistingEntry = Entries[GuiObject]
	if ExistingEntry then
		return ExistingEntry.EnteredEvent , ExistingEntry.LeftEvent
	end
	
	local EnterEvent = Warp.Signal( tostring( RNG:NextNumber() ) )
	local LeftEvent = Warp.Signal( tostring( RNG:NextNumber() ) )
	Entries[GuiObject] = {
		EnteredEvent = EnterEvent,
		LeftEvent = LeftEvent,
		MouseWasIn = false,
	}
	
	if ProcessDeletion then
		local AncestryChangedConnection
		AncestryChangedConnection = GuiObject.AncestryChanged:Connect(function()
			if GuiObject.Parent then
				return
			end
			( EnterEvent :: any ):Destroy();
			( LeftEvent :: any ):Destroy();
			if AncestryChangedConnection then
				AncestryChangedConnection:Disconnect()
			end
			Entries[GuiObject] = nil
		end)
	end
	
	return EnterEvent , LeftEvent
end

return MouseOver

I really like the concept of this resource. I used it in a 2k ccu game and quickly ran into issues I’d like to share!

  • Not performative (resource heavy)
    Maybe I was doing something wrong, but the game was extremely laggy as it’s mainly a ui game and removing this module and using regular mouse events fixed it immediately.

  • Hover Activation through other frames, canvas groups, etc
    Hearing sounds from button hovers when interacting with frames over it was annoying.