# Release notes

> What changed in each Vela release, and the sharp edges still open.

Source: https://docs.astra-void.xyz/vela-rbxts/reference/release-notes/

Every package moves in lockstep, so a fix anywhere bumps everything. Releases are tagged from
`v0.3.0`, and the upstream
[`CHANGELOG.md`](https://github.com/astra-void/vela-rbxts/blob/main/CHANGELOG.md) carries the
per-package detail. Installing for the first time? You are on the current release — skip to
[what is still open](#still-open).

## 0.13.0

The current release: variants you define yourself, responsive ranges, configurable breakpoints,
presets, per-corner radius, and transitions that reach the helper instances.

**State variants.** `addVariant("open", { attribute: "State", equals: "open" })` registers an
`open:` prefix that reads a Roblox attribute off the styled instance. `attr-[State=open]:` reads one
inline. Both compose with every other variant. Only the condition travels to the runtime, so
`open:rounded-lg` lowers `rounded-lg` exactly as a bare `rounded-lg` does. See
[Responsive and input variants](https://docs.astra-void.xyz/vela-rbxts/guides/responsive-and-input-variants.md).

**Responsive ranges.** `max-md:` is the exact complement of `md:`, minimum inclusive and maximum
exclusive, so the two cover every viewport once. They also chain: `md:max-lg:` addresses one bucket.

**Configurable breakpoints.** `theme.screens` is a theme axis like `colors`, so `tablet:` and
`max-tablet:` are two lines of config. The default scale gains `xl` (1280) and `2xl` (1536).

**Presets.** `presets: [gameUiPreset()]` folds a design system's theme, plugins, utilities and
variants into a project in one line. They resolve after the defaults and before the config naming
them, so a project always outranks what it pulled in. A JSON config can inline a preset but cannot
import one. See [presets](https://docs.astra-void.xyz/vela-rbxts/reference/config.md#presets).

**Per-corner radius.** `rounded-l-lg`, `rounded-t-md` and `rounded-tr-[0.625rem]` write the
individual `UICorner` properties. They **square off the corners they do not name**, so
`rounded-r-lg` alone keeps the left side flat. A directional utility beats the all-corner shorthand
on the corners it names, in either order.

**Transitions reach the helper instances.** `hover:rounded-xl` and `hover:border-blue-500` used to
snap, since a radius and a stroke live on `UICorner` and `UIStroke` rather than on the element. They
tween now, and `transition-shadow` joins the property groups. A custom motion driver receives those
tweens with a fourth argument naming the helper. It is additive, so a three-argument driver keeps
working.

**Optional inlay hints.** The editor can show what each class lowers to after the class. Off by
default, behind `velaRbxts.inlayHints.enabled`.

**A `vela.config.ts` is executed once per build**, resolved per directory and re-read only when it
changes. A config that throws is cached alongside one that resolves, so a typo is reported once and
the next edit lifts it.

**Three new prefix diagnostics.** `unknown-breakpoint` fires on a `max-` in front of something that
is not a breakpoint. `malformed-attribute-variant` fires on an `attr-[…]` that does not parse.
`invalid-breakpoint-range` fires on a chain whose bounds leave no viewport.

Sorting ranks the new variants in bands of their own. The existing variants keep the order they had.

## 0.12.8

**The published editor extension carried no config loader**, so a project's `vela.config.ts` was
never read and every key it defined was checked against the default theme. Upgrade if the editor
reports your own theme keys as unknown.

## 0.12.7

**`justify-stretch`**, which sets `UIListLayout.HorizontalFlex` to `Enum.UIFlexAlignment.Fill`.
`items-stretch` reached `VerticalFlex` from the start, but nothing reached the horizontal axis. A
column that wanted its children to fill the width had no class for it.

**A config the editor cannot read is now a notification** naming the file and the reason, rather than
a line in an output channel nobody opens.

## 0.12.6

Three fixes, all a walk that stopped at a boundary too early. The `SurfaceGui` pin and an
`opacity-*` fade now carry through a **fragment, a wrapper or a provider** rather than turning
around at one. A component exported without a name, as `export default (props) => …`, is read as a
component root like the named form beside it.

## 0.12.5

**A `SurfaceGui` keeps its literal pixels.** It takes its pixel space from the part it is drawn on
rather than from the viewport. A `BillboardGui` sizes itself the same way, so following the rem
curve there was wrong. Both are pinned now, on the static and runtime paths alike.
`theme.rem.pinnedUnder` names them, and emptying it puts them back on the curve. A container the
compiler never sees is outside what this reaches. Pin such a project with
`theme.rem: { min: 16, max: 16 }`.

## 0.12.4

**A margin side is one signed slot, so the last class written to it lands.** Padding and a negative
shift used to accumulate separately. Neither could overwrite the other. `-ml-2 -ml-2` shifted by 16
rather than 8, and `ml-4 -ml-2` applied both a padding *and* a shift. A side that ends up negative
moves the element. One that ends up positive pads it. `-mr-*` and `-mb-*` still report
`unsupported-negative-margin`.

**Sorting leaves the whitespace where its author put it**, so a class list written across several
lines is no longer flattened on every save.

Two smaller fixes. `placeholder-transparent` is no longer offered by completion, since Roblox has no
placeholder transparency to lower to. The language server exits on the `exit` notification instead
of waiting for the pipe to close.

## 0.12.3

A patch, mostly in the editor — but two of these move the compiler as well.

**A class an interpolation splices into is left alone.** `` `w-[${width}]` `` reaches the editor as
two pieces. Both were analyzed as whole classes, warning about a class you did not write.

**Whitespace inside an arbitrary value no longer splits it.** `w-[calc(100% - 4px)]` was read as
three classes and collected a diagnostic each. It reports the one it is owed now. The runtime splits
its class strings under the same rule, so a static class and a deferred one tokenize alike.

Four editor fixes behind them. A config pushed by the editor never arrived, so your theme keys
stayed unknown for the session. A file opening with a BOM answered one character to the left.
Completing inside a variant chain deleted the utility behind it. Sorting a value whose bracket never
closes rewrote it.

## 0.12.2

A patch, entirely in the editor — the compiler emits byte for byte what 0.12.0 did.

**The editor understands a class value written as a function.** `className={() => "bg-blue-600"}` is
how a Vide project writes a dynamic one. The walk had no arm for a function at all, so completions,
hover, diagnostics, swatches and sorting saw nothing in it. It follows what a function returns now,
along with template interpolations, `as const`, `satisfies`, string concatenation, and an object's
computed keys and spreads. Two interpolation bugs went with it. A half-typed file no longer reports
`${flag` as an unknown utility, and the sort keeps the whitespace either side of an interpolation.

## 0.12.1

The one to be on if you write Vide. **A Vide project that never named `framework` compiled against
the React host**, emitting an import of `@rbxts/vela-runtime` it has no reason to have installed.
The tsconfig inference only ran for a project with no `vela.config.ts` at all. A Vide project whose
config set a theme or a plugin silently lost it.

## 0.12.0

The largest release since 0.7.0. Offsets became rem units. The runtime became a package instead of a
copy. Branches started resolving at compile time, and Vela learned to emit for a second UI library.

> **Two of these change what you already have**
>
> **Rendering moves on any viewport but the base one**, because every pixel offset now scales. And
> **`className={a ? "x" : "y"}` now reports diagnostics it used to swallow**, so an upgrade can surface
> warnings on code that was already wrong. Neither needs a code change. Nothing about install changes.

**Every pixel offset a utility lowers is a rem unit.** `p-4`, `w-40`, `rounded-lg` and `text-sm` are
measured against the viewport. One rem is 16px at 1920×1020 and scales from there, with no provider,
hook or wrapper involved. Scale-valued utilities are untouched: `w-full` and `translate-x-1/2` stay
fractions of the parent. `TextSize` gains a ceiling of 100, where Roblox stops honouring it. To keep
literal pixels, close the clamp with `theme: { rem: { min: 16, max: 16 } }`. The compiler then drops
the scaling from the emit entirely. See [rem](https://docs.astra-void.xyz/vela-rbxts/guides/theming.md#rem).

**The runtime ships as `@rbxts/vela-runtime` instead of being copied into every file.** Ten
components used to carry ten copies of the same 5,500 lines. Each copy had its own camera
subscription. It is now one ModuleScript the whole place shares. In the reference app `App.luau`
went from 190,260 bytes to 27,912. Setup is unchanged — the package installs with `vela-rbxts` and
sits under the `@rbxts` scope every roblox-ts project already lists.

**The emit sends the theme you changed, not the whole palette.** A module whose host never has to
*parse* a class value sends its scales emptied entirely.

**A branch's classes are resolved at compile time.** `active ? "text-lg" : "text-sm"` names every
token it can apply. The compiler resolves all of them and hands the element the resolved props
alongside the tests that decide them. The full utility set applies inside a branch. A bad utility in
one reports a diagnostic instead of vanishing, and each test is evaluated once however many branches
hang on it. It reads ternaries, `&&`, the literal behind `||`, arrays and object maps. A branch
naming `m-*`, `divide-*`, `animate-*`, `transition*`, a text transform or `opacity-*` still takes
the whole value down the runtime path. See
[dynamic class names](https://docs.astra-void.xyz/vela-rbxts/guides/dynamic-class-names.md#a-branch-lowers-at-compile-time).

**`/N` opacity modifiers lower on every family with a transparency channel.** `border-slate-500/25`,
`divide-white/10` and the gradient stops previously lost exactly the alpha they were written for.
`placeholder-*` is the one family left, and its message now says why.

**Vela emits for [Vide](https://centau.github.io/vide/).** One config key, `framework: "vide"`, or
nothing at all — it is inferred from a `jsxFactory` beginning with `Vide.`. A statically lowered
element is identical under both targets. What differs is the runtime package the emit imports, and
that a dynamic class value is written as a thunk. Two limits are inherent. A component element's
prop names are fixed when it is called, and a `m-*` arriving out of an opaque call is warned rather
than rendered. See [the Vide guide](https://docs.astra-void.xyz/vela-rbxts/guides/vide.md).

Smaller things. Hovers and completions read offsets in rem, and arbitrary values read a `rem` unit.
A base helper a variant rule overwrote no longer leaves two `UIPadding` under one instance. The rule
prop parser learned `Vector2`, `ColorSequence`, `NumberSequence` and `Font`.

## 0.11.1

A patch. The runtime failed to typecheck in a project that sets `noUncheckedIndexedAccess`. That
matters because a `className` carrying a state variant pulls it into the emit, where **your**
compiler options check it.

## 0.11.0

The first release that does not change how a single class lowers. It makes the transformer optional.

**There is a `vela` command now**, for any project that cannot register a roblox-ts transform
plugin. A pinned toolchain, a build system that drives `tsc` itself, or a CI step that wants the
lowered sources as a reviewable artifact. `vela build` mirrors `src` into `.vela/src`, transforming
the files that use `className` and copying everything else byte for byte. `vela watch` re-transforms
on change. Both paths call the same compiler, so the output does not depend on which you pick. An
identical output is never rewritten, and pruning reads a manifest of what an earlier run emitted, so
a file the CLI never wrote is never deleted. See [the CLI reference](https://docs.astra-void.xyz/vela-rbxts/reference/cli.md).

**A whole-tree build stops re-evaluating `vela.config.ts` once per file.** Upgrading from 0.10.0
cannot change your output.

## 0.10.0

Finishes the `opacity-*` work 0.9.0 started, and stops the inlined runtime from crowding Luau's local
register limit.

**`opacity-*` crosses a component boundary, in both directions.** 0.9.0 faded a subtree by walking
the JSX, and that walk ends where the JSX does. A fade written *around* a component reached nothing
it rendered, and one written *on* a component stopped at `BackgroundTransparency`. The alpha now
travels as React context, through a provider that renders no instance, so the tree keeps its shape
and names. Providers are *relative*: two nested fades compose rather than the inner one winning.

**A class value that settles at render time is left whole to the runtime.** An `opacity-*` written
inside a recipe reaches the subtree it is written over.

**`opacity-unreachable-child` is gone.** One difference between the paths remains. The static path
leaves a transparency *you* declared as a prop alone, while a fade arriving as context composes over
both.

**A configured motion driver no longer takes the tree down at mount.** A driver written the
documented way compiles to a method carrying an implicit `self`. The runtime called it detached,
shifting every argument one place left. Methods are called as methods now, and the driver type
states them as methods. The arrow form is a compile error rather than a tree that disappears. See
[the motion driver](https://docs.astra-void.xyz/vela-rbxts/reference/config.md#motion-driver).

**The inlined runtime is grouped into namespaces**, taking the busiest register file in an emitted
file from 177 to 65 against Luau's cap of 200. Both the harness and the compiler crate assert a budget
of 120, so crowding the limit again fails a test rather than your build.

## 0.9.0

**`opacity-*` fades everything the element draws** — every channel the host paints, not just
`BackgroundTransparency`, which was invisible on a label whose background was already transparent.

**And into the subtree written under it.** Roblox has no inherited transparency, so the transformer
walks the JSX. Every instance below the class is handed the running product `1 - (1 - own) * alpha`.
Children written inside an expression count. A `canvasgroup` on the way down ends the walk. Where
this parts ways with a real composite is overlapping siblings: each is faded rather than the group,
so the overlap darkens.

**`opacity-*` stopped being order-dependent.** It is held until the whole class list is read, then
composed over whatever alpha the colours settled on. `opacity-50 bg-slate-700` and
`bg-slate-700 opacity-50` are the same thing.

**The inlined runtime stopped costing every file 96 registers.** It had been failing whole
components with `Out of local registers … exceeded limit 200`. That was against code nobody wrote.

If you are on 0.9.x and see `opacity-unreachable-child`, it is telling the truth — `{props.children}`
and component children stayed out of the compile-time walk. 0.10.0 carries the alpha across both.

## 0.8.0

**Every utility family resolves on the runtime class path.** The runtime host had implemented
roughly a third of what the static path lowers. A component whose `className` arrived as a *value*,
the normal shape for a variant recipe, silently lost most of the rest. Positioning, box constraints,
the grid, gradients, rings, shadows, `z-*`, transforms, `opacity-*`, the ScrollingFrame family and
most of the text families. All of them resolve dynamically now, with the static path's semantics.

This is the release where [dynamic class names](https://docs.astra-void.xyz/vela-rbxts/guides/dynamic-class-names.md) stopped
being a reduced dialect.

**A utility the host element cannot carry is dropped at runtime rather than applied**, since writing
`TextColor3` onto a `Frame` is a hard Roblox error. The static path still writes it — see
[still open](#still-open).

**The runtime host names `UIShadow` by its real class.** The lowercase spelling failed to instantiate
and unwound the whole tree. Fixed on the runtime path only.

## 0.7.0

The release that drew most of the current configuration surface.

- **Plugins.** `addUtilities` registers class names of your own, as a list of existing utilities or as
  Roblox properties written directly, and `theme()` reads the resolved theme. Registered utilities take
  variants, resolve on both paths, and sort *ahead* of the plain utilities. See
  [`plugins`](https://docs.astra-void.xyz/vela-rbxts/reference/config.md#plugins).
- **A motion driver seam.** `setMotionDriver` names a module whose `transition`/`animate` methods
  replace TweenService, one method at a time.
- **`dark:`, `active:` and `focus:` variants.** `dark:` reads a `VelaColorScheme` attribute off the
  local player, since Roblox exposes no color scheme to a running game.
- **`theme.fontFamily` and `font-{family}`**, shipping `sans`, `serif` and `mono` and taking any
  Roblox font family asset.
- **Scrolling frame utilities**, arbitrary length values (`[16px]`, `[50%]` and their negatives), and
  real property filters on `transition-colors`/`-opacity`/`-transform`.
- **Class sorting**, exposed as the `source.sortVelaClasses` source action. See
  [editor setup](https://docs.astra-void.xyz/vela-rbxts/guides/editor-setup.md#sorting-class-names).

## 0.6.0

> **Breaking-ish: grid cells are sized now**
>
> `UIGridLayout` stamps `CellSize` onto every child, and Vela's grid utilities never set it. Every cell fell back to Roblox's 100×100 default, so a `grid-cols-2` of 430px cards collapsed to 100px squares. `grid-cols-N` now divides the axis into N tracks and hands each cell its share of the gap.
>
> The cross axis needs its own answer, so **`auto-rows-*` and `auto-cols-*`** set it from the spacing
> scale. Without one it stays at 100px, so existing grids keep their row extent and gain correct track
> widths — anything compensating for 100px cells needs re-checking.

**`text-{color}` resolves on the runtime path.** The resolver had no `text-` branch at all. Every
text colour in a dynamic class value was dropped without a diagnostic. **`w-*` and `h-*` stop
erasing each other in variant rules**, carrying the 0.5.2 fix into `md:w-32 md:h-32`.

## 0.5.2

**`w-*` and `h-*` stop erasing each other on the runtime path.** `Size` holds both axes. A runtime
rule naming one used to state a whole `UDim2` and zero the other. Worth re-reading if you worked
around this: the axis that lost was left at zero, which also made a scale-based `rounded-full`
compute to nothing.

## 0.5.1

A version-only release — no compiler change.

## 0.5.0

> **Breaking: preflight neutralizes the Roblox host defaults**
>
> Roblox paints every `GuiObject` as an opaque gray box with a 1px border, and a framework that only
> ever *adds* properties can never take that back.
>
> A supported host element **that carries a `className`** now starts from `BackgroundTransparency = 1`
> and `BorderSizePixel = 0`. Anything that actually paints opts back out: a `bg-*` utility,
> `opacity-*`, a gradient stop, or a transparency you declared as a prop. A background painted by a
> variant reopens it at runtime. An element with no `className` is untouched, and so is a
> **component**.
>
> **Anywhere the gray default was load-bearing, the element now renders invisible.** Add the `bg-*` it
> relied on, or set [`preflight: false`](https://docs.astra-void.xyz/vela-rbxts/reference/config.md#preflight).

**`order-*` works under `flex`.** Vela's `UIListLayout` did not set `SortOrder`, and the engine
default is `Name`, so children sorted alphabetically and every `order-*` was a silent no-op. If you
still see `order-*` ignored, the layout instance is one you wrote yourself.

## 0.4.2

**A WebAssembly build of the compiler**, `@vela-rbxts/compiler-wasm`, so lowering can run in a
browser. The [playground](https://docs.astra-void.xyz/vela-playground/) compiles with it.

**Transitions tween from the right starting value**, instead of jumping to the target the first time
a variant introduced the prop. **A base opacity modifier no longer leaks into a variant**, and refs
on runtime-path elements are typed from the tag rather than as `Ref<unknown>`.

## 0.4.1

> **Breakpoints did not work in 0.4.0**
>
> The `0.4.0` runtime helper read `Camera.ViewportSize` **once, at mount**, and Roblox often still reports a 1×1 viewport then. **`sm:`, `md:` and `lg:` never matched, and orientation was always landscape.** `0.4.1` follows the camera's signal, which fixes the initial evaluation and makes
> breakpoints react to resizes. The input variants and `hover:` were never affected.

**`divide-*` drew one separator too many** whenever the element carried a utility that lowers a
helper instance. Config errors name the key that failed, and `tsx-parse-failed` carries a real
source range. Malformed `configJson` is an error rather than a silent fallback to the default theme.

## 0.4.0

Where `hover:`, arbitrary hex colors and `/N` opacity modifiers arrived. Breakpoints and orientation
are broken in this release, as described above. If you are pinned to it, drive reflow from your own
state. See
[Responsive and input variants](https://docs.astra-void.xyz/vela-rbxts/guides/responsive-and-input-variants.md#driving-reflow-yourself).

## Earlier

**0.3.0** landed most of the surface this documentation describes at once. Far-edge positioning,
`content-*`/`self-*`, `order-*`, grid, `translate-*`, `space-x/y-*`, `ring`/`outline`, `leading-*`,
the case transforms and decorations, the whole motion layer, and the structural utilities.

**0.2.1** added `vela.config.json` and the palette `DEFAULT` key, and moved the declaration file to
`src/vela-env.d.ts`. `src/vela-rbxts.d.ts` collides with the package name under a `baseUrl` of
`src`, so the augmentation silently never loaded.

**0.2.0** was the first public release.

## Still open

Known and unfixed as of 0.13.0.

**`gap-*` on a `grid` element emits a conflicting `UIListLayout`** alongside the `UIGridLayout` it
correctly feeds. Set `CellPadding` as a prop and leave `gap-*` off
([utilities reference](https://docs.astra-void.xyz/vela-rbxts/reference/utilities.md#grid)).

**`divide-*` separators are sensitive to explicit `LayoutOrder`** and double-count the parent's
`gap-*` — a consequence of separators being list items
([utilities reference](https://docs.astra-void.xyz/vela-rbxts/reference/utilities.md#divide)).

**The static path emits `<uishadow>` in lowercase**, which `Instance.new` rejects, so a statically
lowered `shadow-*` can fail to instantiate and unwind the tree around it. A dynamic class value on
the same element is unaffected.

**The host guards are not symmetric.** The runtime path drops a utility the host element cannot
carry. The static path warns `unsupported-host-utility` and writes it anyway. A build that ignores
warnings reaches Roblox, which rejects the assignment far from the class list that caused it.

**A fade that arrives as context composes over a transparency you declared yourself.** A consumer
reading the alpha at runtime cannot tell your prop from one Vela lowered. Overlapping siblings under
one `opacity-*` also darken where they overlap.

**A plugin utility that reaches back into itself expands to nothing** — the depth cap terminates
silently, with no diagnostic.

**`divide-transparent` is refused while `divide-white/10` resolves.** The `/N` modifier moved to
every family with a transparency channel in 0.12.0. The `transparent` *keyword* did not.

**Under Vide, a `m-*` the compiler cannot see is warned rather than rendered**, because a margin box
is an instance above one Vide has already parented.

**A theme value that is not a valid expression fails silently, three ways.** Vela splices in the
first expression that parses and drops the rest. A trailing comment is lost, `"#3b82f6"` becomes a
type error, and `"#a1b2c3"` parses as a TypeScript private name and emits bare. See the
[theming guide](https://docs.astra-void.xyz/vela-rbxts/guides/theming.md#values-are-source-code-not-values).

**TypeScript 7 is not supported.** 7.x fails the host adapter's call with
`compiler-invocation-failed: Cannot read properties of undefined (reading 'Latest')` on the first
file. Pin `typescript@5.9.3` or `6.x`. Nothing declares this as a peer range, so the install is clean
and the build is not.

## Platform coverage

Prebuilt binaries do not cover every target: there is no `aarch64-pc-windows-msvc` build, so
**Windows on ARM is unsupported**, and no linux arm64 musl build either. On those two, build the
compiler crate and the language server from source. Full target list in the
[API reference](https://docs.astra-void.xyz/vela-rbxts/reference/api.md#prebuilt-compiler-binaries).

## See also

- [Scope and status](https://docs.astra-void.xyz/vela-rbxts/getting-started/scope-and-status.md) — what is in and out at 0.13.0.
- [Configuration](https://docs.astra-void.xyz/vela-rbxts/reference/config.md#preflight) — the `preflight` opt-out, and
  [`plugins`](https://docs.astra-void.xyz/vela-rbxts/reference/config.md#plugins).
- [Troubleshooting](https://docs.astra-void.xyz/vela-rbxts/guides/troubleshooting.md) — symptom-first.
