@(prop) Property Accessor
@(prop) lets you reference the value of a CSS property that's already
been declared. Write @(border) and you get back whatever border was
last set to in the current selector tree.
The explicit parentheses make the syntax unambiguous:
@(border-width)is clearly a lookup ofborder-width@(--accent-color)works for custom properties- Safe to use inside
{{ }}expressions without ambiguity
This is a compile-time feature - @(prop) resolves during transpilation,
not at browser runtime. The transpiler uses string slice + lazy regex
lookup to find and replace @(border) with the literal value.
How resolution works:
- Cut the CSS zone into slices at
{and}boundaries - When
@(prop)is encountered, search current slice forprop:pattern - Not found? Walk backward through parent slices (parent scopes)
- Extract the value and replace
@(prop)with the literal value - If not found at root of selector tree, preserve
@(prop)unchanged
Why preserve instead of empty string? Lass plays nicely with other
CSS tools. An unresolved @(prop) might be:
- A PostCSS plugin's custom syntax
- A future CSS feature we don't know about yet
- A visible signal that something wasn't resolved (fail visibly)
A few boundaries that @(prop) doesn't cross:
- Sibling selector trees -
.sidebar { border: dotted }is invisible to.main { @(border) }. Each top-level rule is its own tree. - At-rule boundaries -
@(prop)doesn't reach across@media,@layer, or other at-rules (in v0).
valid: same-block reference
@(border) resolves to the border value declared earlier in the same
block.
.box {
border: 1px solid black;
outline: @(border);
}
.box {
border: 1px solid black;
outline: 1px solid black;
}
valid: parent walk-up
If the property isn't in the current block, resolution walks up to
the parent. Here .child doesn't declare border, so @(border)
finds it on .parent.
.parent {
border: solid;
.child {
outline: @(border);
}
}
.parent {
border: solid;
.child {
outline: solid;
}
}
valid: nearest ancestor wins
When multiple ancestors declare the same property, the closest one wins. Same idea as variable scoping in block-scoped languages - the inner declaration shadows the outer.
.parent {
border: solid;
.child {
border: dashed;
.grandchild {
outline: @(border);
}
}
}
.parent {
border: solid;
.child {
border: dashed;
.grandchild {
outline: dashed;
}
}
}
valid: last value before the reference
Within a single block, @(prop) picks up the last value declared before
the reference. Declarations after it don't count.
.child {
border: dashed;
border-left: @(border);
}
.child {
border: dashed;
border-left: dashed;
}
valid: multiple declarations - last one wins
When the same property is declared more than once before the reference,
@(prop) returns the last value. This mirrors CSS cascade behavior
within a single block.
.box {
color: red;
color: blue;
outline-color: @(color);
}
.box {
color: red;
color: blue;
outline-color: blue;
}
valid: inside a {{ }} expression
@(prop) works inside {{ }} too. The compile-time lookup returns a quoted
string that's embedded in the JS expression, enabling JS operations on
CSS values.
.box {
padding: 16px;
margin: {{ parseInt(@(padding)) * 2 }}px;
}
.box {
padding: 16px;
margin: 32px;
}
valid: custom property lookup
@(--custom) works for CSS custom properties. The -- prefix is included
in the lookup pattern.
.box {
--accent-color: blue;
color: @(--accent-color);
}
.box {
--accent-color: blue;
color: blue;
}
valid: sibling trees are isolated - preserved
Properties from sibling selector trees are invisible. Each top-level
rule starts a fresh tree - .sidebar's properties can't leak into
.main. Unresolved @(prop) passes through unchanged, enabling
PostCSS plugins and future CSS features to process it.
.sidebar {
border: dotted;
}
.main {
outline: @(border);
}
.sidebar {
border: dotted;
}
.main {
outline: @(border);
}
valid: doesn't cross @media boundaries - preserved
In v0, @(prop) stops at at-rule boundaries. The @media block is a
separate scope - it can't see properties declared outside.
Unresolved @(prop) passes through unchanged.
This restriction may be relaxed in future versions for specific at-rule types where cross-boundary resolution is unambiguous.
.box {
padding: 16px;
}
@media (min-width: 768px) {
.box {
margin: @(padding);
}
}
.box {
padding: 16px;
}
@media (min-width: 768px) {
.box {
margin: @(padding);
}
}
valid: undeclared property - preserved
Referencing a property that hasn't been declared anywhere in the selector tree passes through unchanged. This enables PostCSS plugins or future CSS features to process it downstream.
.box {
color: @(font-size);
}
.box {
color: @(font-size);
}
valid: forward reference - preserved
@(prop) only looks backward. If the property is declared after the
reference, it doesn't exist yet from the accumulator's point of view.
The unresolved @(prop) passes through unchanged.
.box {
outline: @(border);
border: solid;
}
.box {
outline: @(border);
border: solid;
}
valid: self-reference protection
@(prop) cannot reference its own declaration - only the string
before the current position is searched. This prevents infinite
recursion naturally. The unresolved @(prop) passes through unchanged.
.box {
background: @(background);
}
.box {
background: @(background);
}
valid: build-time @() vs browser-runtime var()
@(--radius) resolves at build time — the value is baked into CSS.
var(--radius) stays as-is for browser runtime resolution.
Both are useful for different purposes.
.card {
--radius: 8px;
--color: blue;
border-radius: @(--radius);
color: var(--color);
}
.card {
--radius: 8px;
--color: blue;
border-radius: 8px;
color: var(--color);
}
valid: custom property parent walk-up
Same as standard properties — if the custom property isn't in the current block, resolution walks up to the parent.
.card {
--spacing: 16px;
.header {
padding: @(--spacing);
}
}
.card {
--spacing: 16px;
.header {
padding: 16px;
}
}