Smooth-Flow is my nitpicky Luau coding style that encases various patterns that I've observed with how I've gone about writing and cleaning up Luau code over the past few years.
It is 99% compatible with StyLua and Roblox's Lua style guide, it just introduces some additional nitpicking and aesthetic changes to help with the readability and overall presentability of the Luau code.
- Write code that groups related actions and assignments.
- Produce aesthetically consistent code through simple rules.
- Favor pre-declared roots to tighten redundant/opaque traversal through nested paths.
- Make code more pleasing to read at a glance, horizontally, vertically, and even diagonally if necessary.
In Luau's new type solver, using --!strict can tend to be more of a hinderance than a necessity, but it is still recommended that you follow the type checking hygiene outlined in this devforum post:
Dependencies will always come first. This is where we route out what modules and services we need.
Some good rules of thumb to follow:
Not only is it redundant to write script.Parent.X over and over again, Roblox has to do (an admittedly negligable amount of) work each time you walk through the DataModel. But more importantly, it's unnecessary noise and clutter. It is much cleaner and less verbose to get references to the roots of each child you intend to access more than once.
local Guard = require(script.Parent.Packages.Guard)
local Example = require(script.Parent.Example)
local Backend = require(script.Parent.Backend)
local Signal = require(script.Parent.Packages.Signal)local Shared = script.Parent
local Packages = Shared.Packages
local Backend = require(Shared.Backend)
local Example = require(Shared.Example)
local Signal = require(Packages.Signal)
local Guard = require(Packages.Guard)It's a good practice to make sure the dependencies you are including are imported as they are named in your codebase, so it's apparent at a glance what is being referred to.
local Guard = require(script.Parent.Packages.Guard)
local sig = require(game.ReplicatedStorage.Library.Packages.Signal)
local OwnerSHIP = require(script.Parent.Ownership)
local emote = require(script.Parent.Emotes)
local Gamepass = require(script.Parent.GamePasses)
local Settings = require(script.Parent.Settings)
local troveLib = require(script.Parent.Packages.Trove)local Shared = script.Parent
local Packages = Shared.Packages
local Guard = require(Packages.Guard)
local Trove = require(Packages.Trove)
local Signal = require(Packages.Signal)
local Settings = require(Shared.Settings)
local Ownership = require(Shared.Ownership)
local GamePasses = require(Shared.GamePasses)3. Prioritize line length over alphabetical, with an emphasis on "smoothing" the shape of "require blocks"
This can appear ascending, descending, or even a mix of the two, but ideally you want to be able to read the list of requires like they are stairs or pyramid shaped.
local Shared = script.Parent
local Packages = Shared.Packages
local Auras = require(Shared.Auras)
local Backend = require(Shared.Backend)
local Emotes = require(Shared.Emotes)
local GamePasses = require(Shared.GamePasses)
local Guard = require(Packages.Guard)
local Marketplace = require(Shared.Marketplace)
local Migration = require(Shared.Migration)
local MusicData = require(Shared.MusicData)
local Mutex = require(Packages.Mutex)
local Network = require(Packages.Network)
local Ownership = require(Shared.Ownership)
local PlayerData = require(Shared.PlayerData)
local Promise = require(Packages.Promise)
local Recents = require(Shared.Recents)
local RunContext = require(Packages.RunContext)
local ServerAudio = require(Shared.ServerAudio)
local Settings = require(Shared.Settings)
local Signal = require(Packages.Signal)
local Tags = require(Packages.Tags)
local Trove = require(Packages.Trove)local Shared = script.Parent
local Packages = Shared.Packages
local Tags = require(Packages.Tags)
local Trove = require(Packages.Trove)
local Guard = require(Packages.Guard)
local Mutex = require(Packages.Mutex)
local Signal = require(Packages.Signal)
local Promise = require(Packages.Promise)
local Network = require(Packages.Network)
local RunContext = require(Packages.RunContext)
local Auras = require(Shared.Auras)
local Emotes = require(Shared.Emotes)
local Backend = require(Shared.Backend)
local Recents = require(Shared.Recents)
local Settings = require(Shared.Settings)
local Migration = require(Shared.Migration)
local MusicData = require(Shared.MusicData)
local Ownership = require(Shared.Ownership)
local GamePasses = require(Shared.GamePasses)
local PlayerData = require(Shared.PlayerData)
local Marketplace = require(Shared.Marketplace)
local ServerAudio = require(Shared.ServerAudio)- Write anonymous functions as
function (...).- Rationale:
functionis a keyword, whereasfunction(...)looks like you are callingfunction(...)as thoughfunctionis a function that can be called. - Keep this consistent in callbacks, hooks, and event handlers.
- Rationale:
- Add a blank line when switching between statement kinds, especially:
- assignment/declaration ->
if/for/while - control flow block -> side-effect call sequence
- local extraction -> guard clause
- assignment/declaration ->
- Use blank lines to mark semantic steps inside a block: extraction, guard, derivation, validation, branching, side effects, async setup, loop body phases, and cleanup.
- Multiline assignments/table fields should usually have a blank line before the next
distinct sibling entry, especially before event handlers or large
e(...)children. - Keep single-action
return/breakpairs and homogeneous scalar groups tight. - Prefer to declare local variables at the top of a group when possible/reasonable, even consider breaking groups up if needed.
- Group related locals with blank lines between groups.
- Group
requirelocals by their root container (Packages,Shared,Components,Hooks, etc.). Within each group, order the lines so the block has a smooth visual shape by line length (ascending, descending, or pyramid is fine); avoid jagged blocks. - Typical grouping order:
- context/hooks
- derived values
- refs/state setters
- helper functions
- When variable names are similar (
minCell,maxCell, etc.), keep them together.
- Prefer an early continue/break/return over deep nesting.
- Use one guard per condition when readability improves.
- Prefer explicit names over terse names (
movedIdoverv). - Keep paired names symmetric (
minCell/maxCell,songId/playlistName).
- Style passes are non-functional by default.
- Avoid broad reformat churn when only a focused region needs cleanup.
- value.Changed:Connect(function(newValue)
+ value.Changed:Connect(function (newValue)
-- ...
end)local function depthForWidth(resolution: Vector2, fov: number, partWidth: number, visibleSize: number): number
local aspect = resolution.X / resolution.Y
+
if aspect ~= aspect then -- NaN guard
return 0
end
+
local resX = math.max(1, resolution.X)
local resY = math.max(1, visibleSize)
+
local hfactor = math.tan(math.rad(fov) / 2)
local wfactor = aspect * hfactor
+
if wfactor <= 0 then
return 0
end
+
return 0.5 * resX * partWidth / (resY * wfactor)
endReact.useEffect(function (): any
local frame = frameRef.current
+
if not frame then
return
endlocal function isAdmin(player: Player): boolean
if not isAllowlistEnabled() then
return true
end
+
if ADMIN_USER_IDS[player.UserId] then
return true
end
+
if ADMIN_GROUP_ID > 0 then
local ok, rank = pcall(function ()
return player:GetRankInGroupAsync(ADMIN_GROUP_ID)
end)
+
if ok and type(rank) == "number" and rank >= ADMIN_MIN_RANK then
return true
end
end
+
return false
end local clone = template:Clone()
-
local model: Model
+
if clone:IsA("Model") then
model = clone
else
-- BasePart / Folder / anything else: wrap it so we have a Model to PivotTo + Scale.
model = Instance.new("Model")
clone.Parent = model
end
+
+ local primary = model:FindFirstChildWhichIsA("BasePart", true)
- model.Name = "ShopAuraPreview"
- local primary = model:FindFirstChildWhichIsA("BasePart", true)
if not primary then
model:Destroy()
return nil, nil
end
+
model.PrimaryPart = primary
+ model.Name = "ShopAuraPreview"
-- ... -- Correct for horizontal FOV distortion off-centre (a wide screen stretches
-- the apparent width away from the middle). Mirrors View3D's `scaleDown`.
local aspect = worldView.X / math.max(1, worldView.Y)
local xScale = center.X / math.max(1, worldView.X)
+
local distortion = 2 * math.abs(xScale - 0.5)
local scaleDown = 1 + (aspect - 1) * distortion
-
local depth = depthForWidth(worldView, cam.FieldOfView, width, visibleSize) * zoom * scaleDown
+
if depth ~= depth or depth <= 0 then
return
end
local ray = cam:ScreenPointToRay(center.X, center.Y)
local frameCenter = ray.Origin + ray.Direction * depth
+
local cf = CFrame.lookAt(frameCenter, frameCenter + ray.Direction, cam.CFrame.UpVector) * FACE_CAMERA
model:PivotTo(cf)
Thanks!