Menu

CSS Utilities

Luna provides atomic CSS utilities with zero client-side runtime for MoonBit applications.

Overview

Luna CSS provides two mechanisms that produce identical class names:

  1. MoonBit Runtime (@css): Generates class names at SSR time

  2. Static Extraction (luna css extract): Extracts CSS from .mbt files at build time

Both use DJB2 hash for deterministic class name generation, ensuring consistency.

How It Works

Build Time                              Runtime (Browser)
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€          โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
.mbt files                              HTML: <div class="_z5et">
    โ”‚                                   CSS:  ._z5et{display:flex}
    โ–ผ
luna css extract โ†’ CSS file             No CSS generation code!
    โ”‚
@css.css("display", "flex")
    โ–ผ
Returns "_z5et"

Basic Usage

MoonBit API

// Single property
let flex_class = @css.css("display", "flex")  // "_z5et"

// Multiple properties
let card = @css.styles([
  ("display", "flex"),
  ("padding", "1rem"),
])  // "_z5et _8ktnx"

// Pseudo-classes
let hover_bg = @css.hover("background", "#2563eb")

// Media queries
let responsive = @css.at_md("padding", "2rem")

// Use in elements
div(class=flex_class, [...])

Import CSS like Tailwind:

// vite.config.ts
import { lunaCss } from "@luna_ui/luna/vite-plugin";

export default defineConfig({
  plugins: [
    lunaCss({
      src: ["src"],       // Source directories with .mbt files
      verbose: true,
    }),
  ],
});
// main.ts
import "virtual:luna.css";  // All extracted CSS

CLI Commands

# Extract all CSS
luna css extract src -o dist/styles.css

# Split by directory (for code splitting)
luna css extract src --split-dir --output-dir dist/css

# Inject into HTML
luna css inject index.html --src src

Vite Plugin Options

interface LunaCssPluginOptions {
  src?: string | string[];     // Source directories
  split?: boolean;             // Enable directory-based splitting
  sharedThreshold?: number;    // Min usages for shared CSS (default: 3)
  verbose?: boolean;           // Enable logging
}

Virtual Modules

ModuleDescription
virtual:luna.cssAll extracted CSS
virtual:luna-shared.cssShared CSS only (split mode)
virtual:luna-chunk/{dir}.cssPer-directory CSS (split mode)

Split Mode

For large applications with code splitting:

lunaCss({
  src: ["src"],
  split: true,
  sharedThreshold: 3,  // CSS used 3+ times โ†’ shared
})
// Import shared + page-specific CSS
import "virtual:luna-shared.css";
import "virtual:luna-chunk/todomvc.css";

Best Practices

1. Use String Literals

Static extraction only works with literals:

// โœ“ Good - extractable
@css.css("display", "flex")

// โœ— Bad - cannot be extracted
let prop = "display"
@css.css(prop, "flex")

When non-literal arguments are used:

  • Static extraction cannot detect them

  • MoonBit runtime still generates the class name

  • But the CSS rule won't exist in the extracted file

  • Result: Element has class but no matching CSS

2. Keep CSS in SSR Code

For zero runtime overhead:

// โœ“ Good - SSR component (server-side only)
fn my_component() -> @static_dom.Node {
  div(class=@css.css("display", "flex"), [...])
}

// โœ— Avoid - Island component (runs in browser)
fn my_island() -> @luna.Node[Unit] {
  // This includes @css module in client bundle!
  div(class=@css.css("display", "flex"), [...])
}

// โœ“ Better for Islands - use pre-computed class string
fn my_island() -> @luna.Node[Unit] {
  div(class="_z5et", [...])  // No @css import needed
}

Why? @static_dom.Node components only run on the server. @luna.Node components run in the browser, so importing @css would include CSS generation code in the client bundle.

3. Dynamic Styling in Islands

// Class name switching
let class_name = if is_active.get() { "_active" } else { "_inactive" }
div(class=class_name, [...])

// CSS custom properties
div(style="--color: " + color.get(), [...])

// Inline styles for dynamic values
div(style="transform: translateX(" + x.get().to_string() + "px)", [...])

Development Mode

Missing CSS Detection

The dev runtime automatically detects missing CSS rules and generates them at runtime with console warnings:

import {
  initCssRuntime,
  css,
  hover,
  hasClass,
  getGeneratedCount
} from "@luna_ui/luna/css/runtime";

// Initialize with options
initCssRuntime({
  warnOnGenerate: true,  // Show warnings (default: true)
  verbose: false,        // Log all generations
});

// If CSS was pre-extracted, this returns existing class
// If missing, generates at runtime and warns
const cls = css("display", "flex");
// Console: [luna-css] Generated at runtime: ._z5et { display: flex }
//          โ†’ Consider running 'luna css extract' to pre-generate CSS

// Check if a class exists in stylesheets
if (!hasClass("_z5et")) {
  console.log("CSS rule missing!");
}

// Get count of runtime-generated rules
console.log(`Generated ${getGeneratedCount()} rules at runtime`);

Use Cases

  1. Development Hot-Reload: Catch missing CSS when editing MoonBit code

  2. Debug Production Issues: Identify CSS rules not captured by static extraction

  3. Dynamic CSS Fallback: Ensure styles work even if extraction misses edge cases

Runtime API

FunctionDescription
initCssRuntime(opts)Initialize with options
css(prop, val)Generate/check base style
hover(prop, val)Generate/check hover style
hasClass(className)Check if class exists in any stylesheet
getGeneratedCss()Get all runtime-generated CSS as string
getGeneratedCount()Count of runtime-generated rules
resetRuntime()Clear runtime state (testing)

Warning: The dev runtime adds ~5KB to your bundle. Only import it during development, not in production builds.

API Reference

Base Styles

FunctionReturnsExample
css(prop, val)Class name"_z5et"
styles(pairs)Space-separated"_z5et _abc"
combine(classes)Joined"_z5et _abc"

Pseudo-classes

FunctionSelector
on(pseudo, prop, val)Custom
hover(prop, val):hover
focus(prop, val):focus
active(prop, val):active

Media Queries

FunctionCondition
media(cond, prop, val)Custom
at_sm(prop, val)min-width: 640px
at_md(prop, val)min-width: 768px
at_lg(prop, val)min-width: 1024px
at_xl(prop, val)min-width: 1280px
dark(prop, val)prefers-color-scheme: dark

Generation (SSR only)

FunctionDescription
generate_css()Base styles only (css(), styles())
generate_full_css()All styles (base + pseudo + media)
reset_all()Clear all registries (testing)

Output example:

/* generate_css() */
._z5et{display:flex}

/* generate_full_css() */
._z5et{display:flex}
._1i41w:hover{border-color:#DB7676}
@media(min-width:768px){._abc{padding:2rem}}

See Also