Skip to main content

Variables

Jess has two explicit variable-reference modes.

ReferenceMeaning
$fooLive reference: read the value currently bound for foo at evaluation time.
$^fooScoped/final lookup.

Choosing a reference​

Use $foo when the value should be live at the point the expression evaluates. Use $^foo when you need the scoped/final lookup behavior.

.card {
color: $theme; // live reference
border: $^theme; // scoped/final lookup
}

Variables are not expression names​

$color reads the variable named color. $(color) does not; inside an expression, color is an ordinary value token. To use a variable inside an expression, keep the variable reference explicit:

$color: red;

.card {
a: $color; // variable lookup -> red
b: $(color); // value token -> color
c: $($color); // expression containing a variable lookup -> red
}

Declarations and assignments​

There are four assignment operators. They differ only in which binding they write and what happens when the name is not yet bound:

OperatorNameWhat it does
$x: valuedeclareBind x in the current scope. If an outer scope already binds x, this shadows it locally — the outer binding is left untouched.
$x := valuereassignWrite the nearest existing binding, wherever it lives up the scope chain. If x is not bound anywhere, this is a compile error.
$x?: valueassign-if-unsetBind x only if it is not already bound. If x already has a value, nothing happens. This is the equivalent of Sass's !default.
$x ::= valueoptional shadowWrite the nearest existing binding, or — if x is unbound anywhere — declare it locally. It always writes; only the fallback declaration is conditional.
$x: 1;
$x := 2;
.a { width: $x; } // 2 — reassigned the existing binding

Reassigning a name that does not exist is an error, rather than quietly creating it:

$x := 2;            // error: live variable $x is undefined

?: assigns only when the name is unset, so an earlier value wins:

$brand: #06c;
$brand?: #f00;
.a { color: $brand; } // #06c — the ?: was skipped

::= is for Sass conversion, not hand-written Jess​

::= exists so an automated SCSS-to-Jess conversion can preserve one Sass rule: a bare $x: written inside a control-flow block reassigns an outer variable when one exists, which is what a @for / @each / @while accumulator relies on. When Jess reads SCSS, it lowers those in-loop $x: declarations to ::= for you.

In hand-written .jess, prefer the explicit operators — : when you mean "declare here" and := when you mean "reassign an existing variable." Reach for ::= only when you genuinely want "reassign if it exists, otherwise declare."

Scope​

A declaration's scope is the nearest enclosing { } block. Rulesets, mixin bodies and function bodies open a scope — and so do control-flow blocks ($if / $for / $while). Braces mean the same thing everywhere: a variable declared with : inside a block is local to it and is gone after the closing brace.

To update a variable that lives outside the block, reassign it with :=:

.box {
$my-color: red;
$if (true) {
$my-color := blue; // updates the outer binding
}
color: $my-color; // blue
}

Writing $my-color: blue inside the $if would declare a block-local variable instead, and color would be red.

The same applies to a loop that builds up a value — $total := $($total + $i) reassigns, while $total: … would start a fresh binding each pass. See Conditionals and iteration.

Not enforced yet for $if and $while

Today only $for enforces this. A : declaration inside $if or $while still leaks into the surrounding block, and := inside $for fails to find an outer binding — jess#232. Write := when you mean "update the outer variable" and your code will be correct either way.

Live and scoped forms​

Every operator also has a $^ scoped/final form ($^x: …, $^x := …, $^x?: …). The sigil chooses which lookup the write and later reads use — live ($x) or scoped/final ($^x) — as described above; it does not change the kind of binding created.

SCSS keeps its own $name: value, !default, and !global syntax at the input boundary; see Sass (SCSS) compatibility.

Block-valued assignments​

Normally, adjacent declarations need semicolon separators. Block-valued assignments are the exception: a variable can be bound to a collection, an anonymous mixin, or a function, and that assignment ends at the block's closing brace. The trailing ; is optional even when another declaration follows:

$colors: {
primary: #06c;
}
$mixin: @{
color: red;
}
$double: @($n) > {
result: $($n * 2);
}

All three are also valid written with a ; after the closing brace. Write whichever you prefer, but you never need one.

Block values stand on their own as the assigned value. To combine data from a block with other values, bind the block first and compose from the binding:

$shadow: {
blur: 4px;
}
$rule: 0 0 $shadow.blur #000;

.box {
box-shadow: $rule;
}

→

.box {
box-shadow: 0 0 4px #000;
}

The same rule applies to collections, anonymous mixins, and function literals.