Examples
EN

Sass/SCSS

Hwaro compiles SCSS at build time with a built-in, pure-Crystal compiler. There is no dart-sass binary to install, no npm toolchain, and no C library, consistent with Hwaro's zero-external-dependency philosophy.

Quick Start

[sass]
enabled = true

Put SCSS files under static/:

static/
├── css/
│   ├── _variables.scss   # partial — never published
│   ├── _mixins.scss      # partial — never published
│   └── style.scss        # entry — compiles to /css/style.css
// static/css/style.scss
@use "variables";
@use "mixins";

.card {
  color: variables.$primary;
  &:hover { color: variables.$accent; }

  @include mixins.respond(768px) {
    padding: 2rem;
  }
}

Every non-partial *.scss compiles to a sibling .css in the output (static/css/style.scss/css/style.css), so stylesheets keep stable URLs:

<link rel="stylesheet" href="{{ url_for(path="/css/style.css") }}">

Rules

Configuration

Option Type Default Description
enabled bool false Enable SCSS compilation
minify bool true Minify compiled CSS (same minifier as the asset pipeline)

Supported Subset

Hwaro implements a practical SCSS subset, the features hand-written site stylesheets actually use:

Feature Support
$variables ✅ with !default / !global, lexical scoping and shadowing
Nested rules ✅ including selector lists (cartesian combination)
& parent selector &:hover, &.mod, BEM &__elem / &--mod
#{...} interpolation ✅ selectors, property names, values, at-rule preludes, strings, url(). Full expressions inside; strings render unquoted at every nesting level, lists included (#{("a", "b")}a, b)
Partials + @use ✅ namespaces (colors.$primary), as x, as *, load-once, with (...) configuration
@forward show / hide filters, as prefix-*
@import (Sass files) ✅ classic global-merge semantics, including a partial that only @forwards (@import "components"components/_index.scss); plain-CSS forms pass through
@mixin / @include ✅ default values, keyword arguments, variadic $args... (extra keywords land in meta.keywords()), spreads (keywords forward through $args...), @content blocks with arguments (@content(1px) / @include m using ($a))
@function / @return ✅ user functions callable in values, defaults/keywords/variadic, recursion
@extend + %placeholders ✅ simple-selector targets, compound unification, !optional; un-extended placeholders never emit (see deviations)
& in SassScript if(&, "&", "") and friends. The parent selector as a value, null at the root
Control flow @if / @else if / @else, @each (with destructuring), @for (through/to, descending), @while
SassScript expressions ✅ arithmetic (+ - * / %, classic slash-division rule), comparisons, and/or/not, strings, lists, maps (see deviations for /)
Unit conversion px/cm/mm/q/in/pt/pc, deg/grad/rad/turn, s/ms, Hz/kHz, dpi/dpcm/dppx convert in arithmetic, comparisons, ==, and math.*
Nested properties font: 12px serif { family: sans; }font, font-family (recursive)
Built-in functions sass:math (incl. log / hypot / trigonometry), sass:string (incl. insert / split), sass:list (incl. zip / set-nth / slash / is-bracketed), sass:map (incl. set / deep-merge / deep-remove), sass:meta (incl. keywords / variable-exists / function-exists / mixin-exists / content-exists / get-function / call), sass:selector (string-level parse / nest / append / unify / replace / is-superselector / simple-selectors; replace shares the @extend subset's limits: compound $original only, no pseudo-class recursion, first prefix order only), sass:color subset (incl. channel / hwb / ie-hex-str) + legacy global names (map-get, nth, darken, if(), …)
Module constants math.$pi, math.$e, math.$epsilon, math.$max-safe-integer, math.$min-safe-integer, math.$max-number, math.$min-number
@debug / @warn / @error @error fails the build with a located message
@at-root ✅ selector and block forms, #{&} suffixing, (with: ...) / (without: ...) queries
@media / @supports in rules ✅ bubbled out of nesting automatically; nested @media merge with and (comma lists cross-multiply); feature values evaluate expressions
@keyframes, @font-face, custom properties ✅ pass through correctly
Plain CSS ✅ any valid .css compiles to itself (whitespace-normalized)

Unknown functions (var(), clamp(), color-mix(), …) pass through untouched. Arguments still evaluate (translate($x * 2, -50%) works), a static calc() folds to its number (see deviations), and a whole-token url($v) / url(ns.$img) substitutes the variable ($ is not valid in a raw URL). A $ embedded in a larger url token (url(plain$x.png)) is valid plain CSS and passes through byte-identical. dart-sass hard-errors there, and evaluates expression contents like url($a + $b) that Hwaro keeps verbatim.

@use "sass:math";
$breakpoints: (sm: 640px, md: 768px, lg: 1024px);

@function rem($px, $base: 16px) { @return math.div($px, $base) * 1rem; }

@mixin respond($name) {
  @if not map-has-key($breakpoints, $name) { @error "unknown breakpoint #{$name}"; }
  @media (min-width: map-get($breakpoints, $name)) { @content; }
}

@each $name, $bp in $breakpoints {
  .container-#{$name} { max-width: $bp - 24px; }
}
@for $i from 1 through 12 {
  .col-#{$i} { width: math.percentage(math.div($i, 12)); }
}
.hero {
  font-size: rem(28px);
  @include respond(md) { font-size: rem(40px); }
}

Colors

Color functions operate on hex literals (#rgb, #rgba, #rrggbb, #rrggbbaa), the CSS color keywords (red, rebeccapurple, transparent), and the legacy comma spellings rgb(…) / rgba(…) / hsl(…) / hsla(…). A color built from hsl() remembers its declared hue/saturation, so hue(hsl(221, 14%, 100%)) answers 221deg:

$brand: #336699;

.button {
  background: $brand;
  border-color: darken($brand, 10%);      // #264d73
  color: scale-color($brand, $lightness: 60%);
  box-shadow: 0 1px 2px rgba($brand, 0.4); // rgba(51, 102, 153, 0.4)
}
Group Functions
Lightness darken, lighten
Saturation saturate, desaturate, grayscale
Hue adjust-hue, complement
Blending mix, invert
Alpha rgba($color, $alpha), opacify / fade-in, transparentize / fade-out
Compound adjust-color, scale-color, change-color
Components red, green, blue, hue, saturation, lightness, alpha / opacity, color.channel
Misc ie-hex-str, color.hwb (both color.hwb($h $w $b) and color.hwb($h, $w, $b))

The same functions are available under sass:color with the modern names: color.adjust, color.scale, color.change, color.mix, color.complement, color.grayscale, color.invert, and the component getters:

@use "sass:color";
.a { border-color: color.scale(#336699, $lightness: -20%); }

A computed color serializes as #rrggbb when opaque and rgba(r, g, b, a) otherwise. A color you don't modify keeps the exact spelling you wrote, so #FFF stays #FFF.

Colors compare by channel across spellings: #ffffff == #FFF, red == #f00, and rgb(255, 0, 0) == #ff0000 are all true (dart-sass semantics).

@extend

@extend works on simple-selector targets (a class, %placeholder, id, element, or pseudo), which covers how real stylesheets (Bootstrap included) use it. The target's compound is unified with the extender's final compound, ancestor compounds are prepended, and un-extended %placeholder rules never reach the output:

%visually-hidden { position: absolute; clip: rect(0 0 0 0); }
.sr-only { @extend %visually-hidden; }
// → .sr-only { position: absolute; clip: rect(0 0 0 0); }

A missing target fails the build with a located error; append !optional to tolerate it. See the deviations list for how the subset differs from dart-sass's full extend algorithm.

Not supported (yet)

Compound units (px*em, px/s, multiplication/division that would need numerator/denominator unit lists), @forward ... with (...), math.random / unique-id() (builds must stay deterministic), the indented .sass syntax, and source maps.

Unsupported directives fail the build with a located error — Hwaro never emits silently broken CSS:

Error [HWARO_E_CONTENT]: Sass: static/css/style.scss:14:3: @forward ... with (...) is not supported

Expression semantics

The compiler's first duty is the plain-CSS guarantee, so expressions follow a two-tier policy:

Deviations from dart-sass

Errors

Compile failures are classified content errors (exit code 5) with path:line:column locations:

Error [HWARO_E_CONTENT]: Sass: static/css/_mixins.scss:7:12: undefined variable: "$primry"

During hwaro serve, errors show in the browser overlay and the previous output stays on disk.

Interplay with Other Features