How to make a transparency transition without CanvasGroups

Hey developers!

I’ve been getting into advanced UI transitions lately and couldn’t find anything on how to automatically tween transparency transitions.

It’s a lot harder than you think. Simply changing the holder object’s transparency won’t do the same for its children. There’s one way to work around it: CanvasGroups.

But there are downsides to using CanvasGroups, such as reduced performance and whatever GroupColor3 is making random objects transparent.

Another way is to just hardcode everything. Just a bunch of tweens, like this:

tweenService:Create(object, tweenInfo, {Transparency = 1}):Play()
tweenService:Create(anotherObject, tweenInfo, {Transparency = 1}):Play()
tweenService:Create(someObject, tweenInfo, {Transparency = 1}):Play()
tweenService:Create(moreObject, tweenInfo, {Transparency = 1}):Play()
tweenService:Create(someOtherObject, tweenInfo, {Transparency = 1}):Play()
tweenService:Create(infiniteObject, tweenInfo, {Transparency = 1}):Play()

But this isn’t very efficient to code, and is bad practice. It’s a bad habit that will be detrimental for you in the long term.

So, I have created an algorithm that will achieve this transition without using CanvasGroups. That’s what I’m going to teach you to create today.

  1. Create a table to store the original transparencies of the GuiObjects. My example:
local originalTransparencies = {}

Then, make an array containing all transparency-related properties. I believe it is the following:

local transparencyProperties = {
	"BackgroundTransparency",
	"TextTransparency",
	"TextStrokeTransparency",
	"Transparency",
	"ImageTransparency",
	"GroupTransparency",
	"ScrollBarImageTransparency"
}

(Let me know if there’s anything missing).

  1. Write a function that allows you to check if a property exists on an instance. My example:
local function checkForProperty(object, property)
	local foundProperty = nil
	local success = pcall(function()
		foundProperty = object[property]
	end)

	if success then
		return foundProperty
	else
		return false
	end
end

Now we’re getting into the good stuff.

  1. Write a function that will tween properties on an object. There are two different ways it will tween: through a dictionary containing the original transparencies of the objects, or creating that dictionary containing the original transparencies, tweening those properties to 1, and returning that dictionary we created.
    My example:
local function tweenProperties(object, tweenInfo, originalTransparencies)
	if originalTransparencies then
		tweenService:Create(object, tweenInfo, originalTransparencies):Play()
	else
		local originalTransparency = {}
		local tweenProperties = {}

		for _, property in ipairs(transparencyProperties) do
			local propertyValue = checkForProperty(object, property)
			if propertyValue then
				if object:IsA("TextLabel") and property == "Transparency" then continue end -- Exception I had to make

				originalTransparency[property] = propertyValue

				if propertyValue < 1 then
					tweenProperties[property] = 1
				end
			end
		end
		tweenService:Create(object, tweenInfo, tweenProperties):Play()

		return originalTransparency
	end
end

(If any of you are wondering about that TextLabel line, there is a property called Transparency that was interfering.)

Finally, we can create the function that will actually transition.

  1. Write a function to transition a GuiObject. It will have 3 parameters: the holder GuiObject, the direction of the transition (is it transitioning in or out?), and an (optional) TweenInfo object to tween properties with.
    If the direction is transitioning in, you will tween the properties of the holder frame using the original transparencies. Then loop through its descendants and do the same. Use this line:
    if not object:IsA("GuiObject") then continue end
    to skip objects that aren’t GuiObjects.
    If the direction is transitioning out, you will tween the properties of the holder frame, and define the original transparency table it returns as a variable. Then you will set that original transparencies dictionary we created in step 1 to that variable. Do the same for it’s descendants, and you are finished.
    My example:
local function transition(holder: GuiObject, direction: number, tweenInfo: TweenInfo)
	if direction == 1 then
		tweenProperties(holder, tweenInfo, originalTransparencies[holder])

		for _, object in ipairs(holder:GetDescendants()) do
			if not object:IsA("GuiObject") then continue end

			if originalTransparencies[object] then
				tweenProperties(object, tweenInfo, originalTransparencies[object])
			end
		end
	elseif direction == 2 then
		local holderOriginalTransparency = tweenProperties(holder, tweenInfo)
		originalTransparencies[holder] = holderOriginalTransparency

		for _, object in ipairs(holder:GetDescendants()) do
			if not object:IsA("GuiObject") then continue end

			local originalTransparency = tweenProperties(object, tweenInfo)
			originalTransparencies[object] = originalTransparency
		end
	end
end

Finally, you are done. Your end script should look something like this:

local tweenService = game:GetService("TweenService")

local originalTransparencies = {}
local transparencyProperties = {
	"BackgroundTransparency",
	"TextTransparency",
	"TextStrokeTransparency",
	"Transparency",
	"ImageTransparency",
	"GroupTransparency",
	"ScrollBarImageTransparency"
}

local function checkForProperty(object, property)
	local foundProperty = nil
	local success = pcall(function()
		foundProperty = object[property]
	end)

	if success then
		return foundProperty
	else
		return false
	end
end

local function tweenProperties(object, tweenInfo, originalTransparencies)
	if originalTransparencies then
		tweenService:Create(object, tweenInfo, originalTransparencies):Play()
	else
		local originalTransparency = {}
		local tweenProperties = {}

		for _, property in ipairs(transparencyProperties) do
			local propertyValue = checkForProperty(object, property)
			if propertyValue then
				if object:IsA("TextLabel") and property == "Transparency" then continue end -- Exception I had to make

				originalTransparency[property] = propertyValue

				if propertyValue < 1 then
					tweenProperties[property] = 1
				end
			end
		end
		tweenService:Create(object, tweenInfo, tweenProperties):Play()

		return originalTransparency
	end
end

local function transition(holder: GuiObject, direction: number, tweenInfo: TweenInfo)
	if direction == 1 then
		tweenProperties(holder, tweenInfo, originalTransparencies[holder])

		for _, object in ipairs(holder:GetDescendants()) do
			if not object:IsA("GuiObject") then continue end

			if originalTransparencies[object] then
				tweenProperties(object, tweenInfo, originalTransparencies[object])
			end
		end
	elseif direction == 2 then
		local holderOriginalTransparency = tweenProperties(holder, tweenInfo)
		originalTransparencies[holder] = holderOriginalTransparency

		for _, object in ipairs(holder:GetDescendants()) do
			if not object:IsA("GuiObject") then continue end

			local originalTransparency = tweenProperties(object, tweenInfo)
			originalTransparencies[object] = originalTransparency
		end
	end
end

And here it is in action:

ezgif-4-8c67e1581b

I’ve also created a module that you can use instead:

It’s only got one function: .transition(holder: GuiObject, direction: number, tweenInfo: TweenInfo).

Good luck with your UI!
Fizzitix

10 Likes

Amazing. Personally I’ve been stuck with either spamming TweenService or using CanvasGroup. I would prefer the code to be a bit shorter but the way it works is quite clever.

Thanks for sharing this. :+1:

1 Like

I think the main purpose of canvas groups was to avoid having the overlapping transparencies make different parts fade faster, though I think this is a good piece of code people often need to implement if they can’t take the performance of canvas groups.

4 Likes

Please correct me if I’m wrong, but would adding custom attributes to the UI instances not simplify this? Using custom attributes gives you more control over which instances are affected. I’m not sure how performant it’d be, but I personally prefer having that extra control without having to implement a list of elements to ignore if I choose to do so.

for i, v in next, ScreenGui:GetDescendants() do
	if v:GetAttribute("SetTransparency") ~= nil then
        --Add instance to table for later use or tween
	end
end

Either way, this is a great resource and will surely save people a lot of time and effort. Thank you for releasing it.

1 Like

Sure you can! But I can’t imagine it being much less of a burden than doing this:

tweenService:Create(object, tweenInfo, {Transparency = 1}):Play()
tweenService:Create(anotherObject, tweenInfo, {Transparency = 1}):Play()
tweenService:Create(someObject, tweenInfo, {Transparency = 1}):Play()
tweenService:Create(moreObject, tweenInfo, {Transparency = 1}):Play()
tweenService:Create(someOtherObject, tweenInfo, {Transparency = 1}):Play()
tweenService:Create(infiniteObject, tweenInfo, {Transparency = 1}):Play()

If you could find a way to automate the process, such as a plugin, go for it!
Fizzitix