demo-css-macro.mov
A compile-time CSS string interpolator with nested selector flattening, syntax validation, and type-safe class name access via Scala 3 named tuples.
- Nested selector flattening: Write SCSS-like nested CSS, get flat CSS output
- Parent selector (
&): Use&:hover,&--modifierfor BEM-style selectors - CSS combinators: Full support for
>,+,~combinators - String interpolation: Use
$variableor${expression}for dynamic CSS values - Compile-time validation: Catches unbalanced braces and missing selectors at compile-time
- Compile-time class extraction: All class names (including generated ones) extracted at compile-time
- Type-safe class names: Access class names as fields on a named tuple with full IDE support
- Named tuple return: Returns
(css: String, classNames: (...))tuple
import www.CssMacro.css
val primaryColor = "#3498db"
val padding = "16px"
val styles = css"""
.container {
padding: $padding;
background: white;
}
.button {
background: $primaryColor;
color: white;
}
"""
// Access the CSS string
println(styles.css)
// Access class names (type-safe!)
println(styles.classNames.container) // "container"
println(styles.classNames.button) // "button"Write nested CSS like SCSS - it gets flattened automatically:
val styles = css"""
.card {
background: white;
border: 1px solid #ccc;
.header {
font-size: 24px;
font-weight: bold;
}
.content {
padding: 16px;
}
}
"""
// Output CSS is flattened:
// .card { background: white; border: 1px solid #ccc; }
// .card .header { font-size: 24px; font-weight: bold; }
// .card .content { padding: 16px; }
println(styles.classNames.card) // "card"
println(styles.classNames.header) // "header"
println(styles.classNames.content) // "content"Use & to reference the parent selector - perfect for BEM methodology:
val styles = css"""
.button {
color: blue;
padding: 8px 16px;
&:hover {
color: darkblue;
}
&--primary {
background: blue;
color: white;
}
&--secondary {
background: gray;
}
}
"""
// Output CSS:
// .button { color: blue; padding: 8px 16px; }
// .button:hover { color: darkblue; }
// .button--primary { background: blue; color: white; }
// .button--secondary { background: gray; }
// All generated class names are available!
println(styles.classNames.button) // "button"
println(styles.classNames.`button--primary`) // "button--primary"
println(styles.classNames.`button--secondary`) // "button--secondary"val styles = css"""
.nav {
display: flex;
&__list {
list-style: none;
&-item {
padding: 8px;
&--active {
color: blue;
}
}
}
}
"""
// Generates:
// .nav { display: flex; }
// .nav__list { list-style: none; }
// .nav__list-item { padding: 8px; }
// .nav__list-item--active { color: blue; }
println(styles.classNames.nav) // "nav"
println(styles.classNames.`nav__list`) // "nav__list"
println(styles.classNames.`nav__list-item`) // "nav__list-item"
println(styles.classNames.`nav__list-item--active`) // "nav__list-item--active"Full support for child (>), adjacent sibling (+), and general sibling (~) combinators:
val styles = css"""
.menu {
display: flex;
> .item {
padding: 8px;
}
+ .sibling {
margin-top: 16px;
}
~ .general {
opacity: 0.5;
}
}
"""
// Generates:
// .menu { display: flex; }
// .menu > .item { padding: 8px; }
// .menu + .sibling { margin-top: 16px; }
// .menu ~ .general { opacity: 0.5; }val styles = css"""
.tooltip {
position: relative;
&::before {
content: "";
position: absolute;
}
&:hover {
opacity: 1;
}
&:not(:last-child) {
margin-bottom: 8px;
}
&:nth-child(odd) {
background: #f5f5f5;
}
}
"""
// Generates:
// .tooltip { position: relative; }
// .tooltip::before { content: ""; position: absolute; }
// .tooltip:hover { opacity: 1; }
// .tooltip:not(:last-child) { margin-bottom: 8px; }
// .tooltip:nth-child(odd) { background: #f5f5f5; }val styles = css"""
.input {
border: 1px solid gray;
&[disabled] {
opacity: 0.5;
}
&[type="text"] {
padding: 8px;
}
&[data-state="active"] {
border-color: blue;
}
}
"""
// Generates:
// .input { border: 1px solid gray; }
// .input[disabled] { opacity: 0.5; }
// .input[type="text"] { padding: 8px; }
// .input[data-state="active"] { border-color: blue; }val styles = css"""
.article {
h1 {
font-size: 32px;
}
p {
line-height: 1.6;
}
a {
color: blue;
}
}
"""
// Generates:
// .article h1 { font-size: 32px; }
// .article p { line-height: 1.6; }
// .article a { color: blue; }CSS class names containing hyphens are preserved as-is. Use Scala backticks to access them:
val styles = css"""
.callout--warning {
background-color: yellow;
}
.my-component {
display: block;
}
"""
// Use backticks for names with hyphens
println(styles.classNames.`callout--warning`) // "callout--warning"
println(styles.classNames.`my-component`) // "my-component"The interpolator supports any Scala expression:
val baseSize = 8
val theme = Map("primary" -> "#3498db", "secondary" -> "#2ecc71")
val styles = css"""
.card {
padding: ${baseSize * 2}px;
margin: ${baseSize}px;
background: ${theme("primary")};
}
"""
// Interpolation also works in selectors
val state = "hover"
val modifier = "primary"
val buttonStyles = css"""
.btn {
&:$state {
opacity: 0.8;
}
&--$modifier {
background: blue;
}
}
"""
// Generates: .btn:hover and .btn--primaryArbitrary nesting depth is supported:
val styles = css"""
.app {
.sidebar {
.nav {
.item {
.icon {
color: blue;
}
}
}
}
}
"""
// Generates: .app .sidebar .nav .item .icon { color: blue; }Full support for modern CSS features:
val styles = css"""
.component {
/* CSS Custom Properties */
--primary-color: blue;
--spacing: 8px;
color: var(--primary-color);
/* calc() expressions */
width: calc(100% - 32px);
height: calc(100vh - 64px);
/* rgba/hsla colors */
background: rgba(0, 0, 0, 0.5);
border-color: hsla(210, 50%, 50%, 0.8);
/* transforms */
transform: translateX(100px) rotate(45deg) scale(1.5);
/* transitions */
transition: all 0.3s ease-in-out;
/* box-shadow with multiple values */
box-shadow: 0 2px 4px rgba(0,0,0,0.1), 0 4px 8px rgba(0,0,0,0.1);
/* font-family with quotes */
font-family: "Helvetica Neue", Arial, sans-serif;
/* URLs */
background-image: url(https://example.com/img.png);
/* !important */
display: block !important;
}
.icon::before {
/* Content with special characters - braces in strings are handled correctly */
content: "{";
}
"""The macro validates CSS syntax at compile-time and reports helpful errors:
// Missing closing brace - COMPILE ERROR
val styles = css".button { color: red;"
// Error: CSS syntax error: Unbalanced braces: 1 unclosed '{'
// Extra closing brace - COMPILE ERROR
val styles = css".button { color: red; }}"
// Error: CSS syntax error: Unexpected '}' at position X - no matching '{'// Missing selector before brace - COMPILE ERROR
val styles = css"{ color: red; }"
// Error: CSS syntax error: Missing selector before '{'Braces inside quoted strings are correctly ignored:
// This compiles fine - braces in strings don't count
val styles = css"""
.icon::before {
content: "{";
}
.icon::after {
content: "}";
}
"""The css interpolator returns a named tuple with two fields:
| Field | Type | Description |
|---|---|---|
css |
String |
The flattened CSS string with interpolated values |
classNames |
Named Tuple | A named tuple where each field is a class name |
- At compile-time:
- Validates CSS syntax (balanced braces, selectors present)
- Flattens nested CSS selectors
- Resolves
&parent references - Extracts all class names from the flattened output
- Builds a typed named tuple for class name access
- At runtime (with interpolations): The CSS string is built and flattened
- Return value: A named tuple with the CSS string and all extracted class names
# Run tests
scala-cli test src
# Run example
scala-cli run src