Skip to content

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();
… and in your stylesheet
.demo {
  font-size: pow(2, 3) * 1px;
}

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));
};
.demo { border: button(solid, $size: 2rem); }   // registered as "button($args...)"

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;
};
.demo { color: theme-color("primary"); }

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.