Custom functions
A custom function is a piece of Java that SCSS can call like any built-in. Register it under its SCSS signature and jsass marshals the arguments and the return value across the engine boundary for you.
SassCallback pow = ctx -> {
var args = ctx.getArgs();
var base = (SassNumber) args.get(0);
var exp = (SassNumber) args.get(1);
return SassNumber.of(Math.pow(base.getValue(), exp.getValue()), base.getUnit());
};
var options = StringOptions.builder()
.url(URI.create("virtual:root.scss"))
.functions(Map.of("pow($base, $exp)", SassFunction.builder().callback(pow).build()))
.build();
The map key is the full SCSS signature, argument names included — that is how dart-sass learns the
arity and the parameter names. The SassFunctionContext handed to the callback carries that same
signature plus the args in declaration order, as an unmodifiable list.
Values
All values crossing the boundary implement the sealed interface SassValue. There are eight
implementations, plus SassArgumentList, a subclass of SassList. Each has static factories; the
constructors are not public.
| Type | Create it with | Read it with |
|---|---|---|
SassNumber |
SassNumber.of(3.5, "px"), SassNumber.unitless(42) |
getValue(), getUnit() |
SassString |
SassString.quoted("a"), SassString.unquoted("a") |
getValue(), isQuoted() |
SassColor |
SassColor.rgb(59, 130, 246, 1.0) and one factory per space |
getSpace(), getChannel1() … getAlpha() |
SassBoolean |
SassBoolean.of(true), SassBoolean.TRUE, SassBoolean.FALSE |
isValue() |
SassNull |
SassNull.NULL |
— |
SassList |
SassList.comma(items), .space(items), .slash(items) |
getContents(), getSeparator(), isBracketed() |
SassMap |
SassMap.of(map) |
getContents() |
SassCalculation |
SassCalculation.calc(v), .min(list), .max(list), .clamp(min, v, max) |
getName(), getArguments() |
SassArgumentList |
SassArgumentList.of(items, keywords, separator) |
everything a SassList has, plus getKeywords() |
Lists and maps defensive-copy their contents and are immutable afterwards; SassList.bracketed()
returns a bracketed copy rather than mutating.
An exhaustive switch needs a SassCalculation case
SassCalculation is new in 6.0.0-alpha.6, so a pattern-matching switch over SassValue
without a default branch stops compiling until you add it. SassArgumentList needs no case
of its own: case SassList matches it.
Colors
SassColor carries a color-space tag, three channels and an alpha. Use the factory for the space
you are working in — the channel meaning follows from it:
| Space | Factory | Channels |
|---|---|---|
RGB, SRGB, SRGB_LINEAR, DISPLAY_P3, A98_RGB, PROPHOTO_RGB, REC2020 |
SassColor.rgb(r, g, b, a), .srgb(…), .srgbLinear(…), .displayP3(…), .a98Rgb(…), .prophotoRgb(…), .rec2020(…) |
red, green, blue |
HSL |
SassColor.hsl(h, s, l, a) |
hue, saturation, lightness |
HWB |
SassColor.hwb(h, w, b, a) |
hue, whiteness, blackness |
LAB / OKLAB |
SassColor.lab(…) / SassColor.oklab(…) |
lightness, a, b |
LCH / OKLCH |
SassColor.lch(…) / SassColor.oklch(…) |
lightness, chroma, hue |
XYZ_D50 / XYZ_D65 |
SassColor.xyzD50(…) / .xyzD65(…), .xyz(…) |
x, y, z |
rgb() validates its channels (0–255) and the alpha (0–1); the other spaces accept unbounded
channels — gamut mapping is deliberately out of scope.
RGB does not survive a round trip
dart-sass normalises the legacy rgb space to srgb on read-back. A color built with
SassColor.rgb(…) therefore comes back as ColorSpace.SRGB. Use SassColor.srgb(…) when
you compare a returned color against an expected one.
Calculations
A calculation that dart-sass cannot simplify yet — calc(1px + 10%), min(1px, 10%),
clamp(1rem, 2vw, 3rem), anything with a var() in it — reaches a custom function as a
SassCalculation: a name and a list of CalculationValue arguments. A CalculationValue is a
SassNumber, an unquoted SassString (such as var(--x)), a nested SassCalculation or a
CalculationOperation, the binary +, -, * or / between two of them:
// calc(1px + 10%) arrives as
SassCalculation.calc(
CalculationOperation.of(Operator.PLUS, SassNumber.of(1, "px"), SassNumber.of(10, "%")));
CalculationOperation has getOperator(), getLeft() and getRight(); Operator is the nested
enum CalculationOperation.Operator (PLUS, MINUS, TIMES, DIVIDE). A quoted string is
rejected with an IllegalArgumentException — no calculation may contain one. Interpolation inside
a calculation arrives as an unquoted string in parentheses.
Returning a calculation works the same way:
SassCallback gutter = ctx -> {
var min = (SassNumber) ctx.getArgs().get(0);
var max = (SassNumber) ctx.getArgs().get(1);
return SassCalculation.clamp(min, SassNumber.of(4, "vw"), max);
};
SassCallback fullWidthMinus = ctx -> {
var inset = (SassNumber) ctx.getArgs().get(0);
return SassCalculation.calc(
CalculationOperation.of(Operator.MINUS, SassNumber.of(100, "%"), inset));
};
Registered as gutter($min, $max) and full-width-minus($inset):
.demo {
padding: gutter(1rem, 3rem); // clamp(1rem, 4vw, 3rem)
width: full-width-minus(2rem); // calc(100% - 2rem)
}
Only calc, min, max and clamp can be returned
dart-sass also hands functions calculations of other names — round(), abs(), mod() and
the rest. You can read them, but you cannot return them, not even unchanged: dart-sass's
JavaScript API constructs only calc() (with exactly one argument), min(), max() and
clamp() (with exactly three). jsass enforces the same rule on every engine, so returning
round(10%, 1px) — or a received single-argument clamp(var(--x)) — fails the compile with
Unsupported Sass value: …, also when it is nested inside a list. The public factories
only build the returnable shapes.
On the JavaScript engines, returning a calculation that contains a CalculationOperation — such as
calc(100% - 2rem) above — needs dart-sass 1.98.0 or newer; older releases fail inside
dart-sass. The embedded engine has no such floor (see compatibility).
Argument lists and keyword arguments
A rest parameter ($args...) arrives as a SassArgumentList: the positional arguments as an
ordinary list, plus the keyword arguments the caller passed, by name without the $, in call
order:
SassCallback button = ctx -> {
var args = (SassArgumentList) ctx.getArgs().get(0);
var keywords = args.getKeywords(); // {"size": 2rem}, marks them read
var size = keywords.getOrDefault("size", SassNumber.of(1, "rem"));
return SassList.space(List.of(args.getContents().get(0), size));
};
As in dart-sass, keyword arguments a function never looks at are an error: if the callback does not
call getKeywords(), h(1, $x: 2) fails the compile with No parameter named $x. (and
No parameters named $x, $y or $z. for several). toString(), equals() and hashCode() do not
count as reading; wereKeywordsAccessed() tells whether something did.
Only the list created for the function's own rest parameter is checked. A SassArgumentList
that reaches Java as an ordinary argument — a Sass function forwarding its $args — belongs to the
call that created it, and dart-sass checks it when that call returns.
Forwarded argument lists: read the keywords
When a Sass function hands its own $args to a Java function as an ordinary argument, the
engines differ. The JavaScript engines always count those keywords as read; the embedded engine
counts them only if the Java side calls getKeywords(). A wrapper whose keywords nobody reads
therefore passes on V8, Node and GraalJS but fails on the embedded engine with
No parameter named …. Read them — in Java with getKeywords(), or in Sass with
meta.keywords($args) — and every engine behaves the same.
A function may also return a SassArgumentList (SassArgumentList.of(items, keywords,
SassList.Separator.COMMA)); Sass treats it as the list it is.
A lookup function
Anything the callback can reach is fair game — a configuration object, a design-token map, a database:
private static final Map<String, SassColor> THEME = Map.of(
"primary", SassColor.rgb(59, 130, 246, 1.0),
"secondary", SassColor.hsl(217, 91, 60, 1.0));
SassCallback themeColor = ctx -> {
var name = ((SassString) ctx.getArgs().get(0)).getValue();
var color = THEME.get(name);
if (null == color) {
throw new IllegalArgumentException("Unknown theme color: " + name);
}
return color;
};
When a function fails
Argument typing is the callback's job: cast to the SassValue subtype you expect. If the cast
fails, or the callback throws anything else, jsass wraps it as a
SassFunctionExecutionException and dart-sass reports it as a compilation error pointing at the
call site. The original exception stays available through getCause().
In other words: throwing from a custom function is a supported way to reject bad input. It produces a normal Sass error, not a crashed compiler.