It’s really up to you but I’ll share how I would do it personally.
I would probably make a class for the individual components of your main menu, like custom button controllers, custom UI objects etc, not just have a whole class for an object that will only exist one time.
For example, this is our class library, there will (I think) always be more than one of these at any given time, with some exceptions like _types and UtilityObjects which is just a utility module.

Particular to the module I try to use events whenever possible. For example in my ButtonGeneric instead of having a :OnClicked method, I have an event that lets me bind an arbitrary function to an event, in place of having a generic method that gets called whenever I need to add functionality. I make necessary connections and signals in my constructor, then if another script needs to subscribe or disconnect from one of my object’s events, it’s up to said script to do this. Note I don’t ever call the class methods unless it’s in an event, the initialization of an object should be done inside of the constructor.
function module.new(object: Frame)
-- avoid duplicate creation of buttons, it's not anything necessary, it's just that my main UIController module acts as a "pool" of all my objects which allows them to be easily accessed by other objects.
local objectAlreadyCreated = uiController:GetObjectByReference(object)
if objectAlreadyCreated then
return objectAlreadyCreated
end
-- define properties inside of table constructor when possible, immediately set metatable:
local self = setmetatable({
CaptureBounds = (object:FindFirstChild('CaptureBounds') or utilObjects.captureBounds(object)) :: TextButton;
Bottom = object:FindFirstChild('Bottom') or utilObjects.offsetFrame(object, 'Bottom', DARKEN_AMOUNT);
Top = object:FindFirstChild('Top') or utilObjects.offsetFrame(object, 'Top');
_frameRef = object;
Enabled = true
}, module)
-- Our buttons are composed of 2 parts: top and bottom, so set those up & create mouse button down & up tweens:
self.Bottom.Name = 'Bottom'
self.Top.Name = 'Top'
self.Top:SetAttribute('OriginalBackgroundColor3', self.Top.BackgroundColor3)
self.DownTweens = {
tweenService:Create(self.Top, TweenInfo.new(0.125), {
BackgroundColor3 = self.Bottom.BackgroundColor3;
AnchorPoint = Vector2.new(0,1);
Position = UDim2.fromScale(0,1)
})
}
self.UpTweens = {
tweenService:Create(self.Top, TweenInfo.new(0.125), {
BackgroundColor3 = self.Top.BackgroundColor3;
AnchorPoint = Vector2.new();
Position = UDim2.new()
});
}
for i,v in ipairs(self.Top:GetDescendants()) do
local madeGoals, goals = getTweenGoals(v)
if madeGoals then
table.insert(self.DownTweens, tweenService:Create(v, TweenInfo.new(0.125), goals.GoalsDown))
table.insert(self.UpTweens, tweenService:Create(v, TweenInfo.new(0.125), goals.GoalsUp))
end
end
-- clear out excess instances
for i,v in ipairs(object:GetChildren()) do
if v:GetAttribute('HasCloned') then
v:Destroy()
end
end
-- create events:
self.MouseButton1Click = framework.signal.new()
-- create connections that call the object's own methods, do this sparingly (eg. when working with button core functionality):
self.CaptureBounds.MouseButton1Down:Connect(function()
if not self.Enabled then
return
end
self:Down()
end)
self.CaptureBounds.MouseButton1Up:Connect(function()
if not self.Enabled then
return
end
self:Up()
end)
self.CaptureBounds.MouseLeave:Connect(function()
if not self.Enabled then
return
end
self:Up()
end)
self.CaptureBounds.MouseButton1Click:Connect(function()
if not self.Enabled then
return
end
framework.soundManager:Play(optionsHandler:GetOptionValue('SoundVolume').Value)
if not self._frameRef:GetAttribute('IgnoreHaptics') then
framework.inputManager:PlayHaptic()
end
self.MouseButton1Click:Fire()
end)
-- finally make the object's background transparency 1, now the object's creation is complete.
object.BackgroundTransparency = 1
return self
end
So to summarize, my constructor:
1- Creates properties and related objects (including signals) and when necessary exposes them to other scripts.
2- Sets up core functionality of the object (i.e. hook into real button (instance) events and do visuals and whatnot, and fire events)
It is up to other scripts to hook into my button object’s events and make my object behave in some way and how the button interacts with other buttons, etc.