#Style
A style is a named bag of values that declarations bind to. Change a field and every instance bound to it updates, everywhere, immediately.
It exists because the same handful of properties end up copied across every button, every card, every label. A style names them once.
Scope:LoadStyle({
name = "Dark",
Surface = Color3.fromRGB(20, 20, 26),
Ink = Color3.fromRGB(240, 240, 250),
Accent = Color3.fromRGB(88, 101, 242),
Title = Enum.Font.GothamBold,
Radius = 12,
})
Scope:Capture({
ClassName = "Frame",
BackgroundColor3 = style("Dark")["Surface"],
{
ClassName = "TextLabel",
TextColor3 = style("Dark")["Ink"],
Font = style("Dark")["Title"],
},
})
A field is just a value, so a style is not limited to colours. Fonts, sizes,
numbers, UDim2, EnumItem - anything you would assign to a property.
#Scope:LoadStyle
Scope:LoadStyle(Declaration: { name: string, [string]: any }) -> Style
Creates the style if it does not exist, updates it if it does, and returns
the sheet. name identifies it and is not itself a field.
const Dark = Scope:LoadStyle({ name = "Dark", Surface = Ink, Radius = 12 })
Calling it again with the same name updates in bulk. Fields you do not mention are left alone, so you can nudge two values without restating the whole style.
Scope:LoadStyle({ name = "Dark", Surface = Accent, Radius = 20 })
A declaration without a string name raises.
#style
style(Name: string)[Field: string]
The declaration keyword. Reads as style("Dark")["Surface"], or
style("Dark").Surface if you prefer - they are the same thing.
It produces a binding, not a value. The property is filled in at mount, which means a tree can be declared before its style exists:
const Interface = Scope:Capture({
ClassName = "Frame",
BackgroundColor3 = style("Dark")["Surface"],
})
Scope:LoadStyle({ name = "Dark", Surface = Ink }) -- still fine
Interface:Mount()
Naming a style that was never loaded raises at mount and tells you which one.
Properties only
Bindings work on properties. attributes and tags take literal values.
#The sheet
LoadStyle returns a Style.
#Get
Dark:Get("Radius") --> 12
#Set
Dark:Set("Surface", Accent)
Writes one field and updates every bound instance. Writing the value it
already holds does nothing, so setting a style in a loop or re-loading it on
a heartbeat is cheap and will not churn Changed.
#Name
Dark.Name --> "Dark"
#Fields cannot be tables
Scope:LoadStyle({ name = "Bad", Layout = { 1, 2, 3 } })
--> style "Bad" field "Layout" cannot be a table
A field has to be a single assignable value. Roblox datatypes are fine -
Color3, UDim2, Vector2 and friends are not Lua tables, so they pass.
#Lifecycle
Bindings follow the same rules as every other connection in Vision.
| when | what happens |
|---|---|
Mount |
fields are read and the instances subscribe |
| field changes | every bound instance is written |
Cleanup |
the tree unsubscribes; a destroyed instance is never written again |
Mount again |
rebinds, and wakes with whatever the field holds now |
Clone |
the copy inherits the bindings |
Scope:Release |
every sheet drops its users |
A sleeping Vision costs a style nothing. It is not subscribed, it is not written to, and it picks up the current values when it wakes - not the ones that were current when it went to sleep.
Nothing is pinned
A style holds its bound instances weakly and unsubscribes on cleanup, so it can never keep a discarded or sleeping Vision alive.
#Swapping themes
Because a field updates in place, a theme swap is a handful of writes rather than a rebuild.
const Themes = {
Dark = { Surface = Color3.fromRGB(20, 20, 26), Ink = Color3.fromRGB(240, 240, 250) },
Light = { Surface = Color3.fromRGB(246, 246, 250), Ink = Color3.fromRGB(24, 24, 30) },
}
const function Wear(Which: string)
const Skin = Themes[Which]
Scope:LoadStyle({
name = "Dark",
Surface = Skin.Surface,
Ink = Skin.Ink,
})
end
Nothing rebuilds, nothing remounts, and any tree that is asleep picks up the new values the next time it mounts.
To fade between themes rather than snap, animate the fields.
#Animating a style
Scope:SpringStyle(Info: TweenInfo, Target, Fields: { [string]: any }) -> Linker
Scope:PhysicsStyle(Period: number, Damping: number, Target, Fields) -> Linker
Animates the fields themselves, so every instance bound to them moves at
once. Target is a sheet, a style name, or a list of either.
Scope:SpringStyle(TweenInfo.new(0.4), Dark, {
Surface = Color3.fromRGB(246, 246, 250),
Ink = Color3.fromRGB(24, 24, 30),
})
That is a whole theme crossfading, in one call, however many trees are using it. The solver form carries velocity through an interruption:
Scope:PhysicsStyle(0.5, 0.7, "Dark", { Radius = 20 })
Both return a Linker you can Await or Cancel,
and both claim (sheet, field), so starting a new animation on a field
cancels whatever was driving it. That is the same claim system the instance
and value animations use, so a style animation and a SpringInstance on the
same property still resolve sanely.
A field with no current value cannot be animated from, and raises saying so.
PhysicsStyle also raises on a field whose type has no numeric components,
such as an EnumItem - use SpringStyle for those, which switches at the
halfway point.
Sleeping trees are not written to during a style animation and pick up the finished value when they wake.