March Docs

IslandCss

Bastion.IslandCss — scoped CSS for WASM islands.

Transforms flat CSS into attribute-scoped CSS so island styles don't leak to the rest of the page or to other islands.

Scoping mechanism:

  1. Generate a unique data attribute for each island module.
   e.g. "SearchBar" → data-b-search-bar
2. Prefix every CSS selector with the attribute selector.
   e.g. ".btn { … }" → "[data-b-search-bar] .btn { … }"
3. Islands.wrap/4 adds the data attribute to the island's root element,
   so the scoped selectors match.

@ rules (media queries, keyframes, etc.) and special selectors (:root, html, body, *) are passed through unmodified.

Usage:

let css    = ".counter { display: flex; }\n.count { font-size: 2rem; }"
let scoped = IslandCss.scope_css(css, "Counter")
-- "[data-b-counter] .counter { display: flex; }\n[data-b-counter] .count { font-size: 2rem; }"

let attr = IslandCss.attr_name("SearchBar")  -- "data-b-search-bar"

Island declaration (Bastion convention):

mod MyApp.Islands.Counter do
  import Bastion.Island

  fn styles() : String do
    ".counter { display: flex; gap: 8px; }" ++
    ".count { font-size: 2rem; font-weight: bold; }"
  end

  -- ... init, update, view ...
end

Functions

fnattr_nameattr_name(island_name : String) : String#

Return the HTML attribute name (without brackets) for an island.

This is the attribute added to the island's root element by Islands.wrap.

Example:
  IslandCss.attr_name("Counter")    -- "data-b-counter"
  IslandCss.attr_name("SearchBar")  -- "data-b-search-bar"
fnattribute_forattribute_for(island_name : String) : String#

Return the CSS attribute selector used to scope an island's styles.

Example:
  IslandCss.attribute_for("SearchBar")  -- "[data-b-search-bar]"
  IslandCss.attribute_for("Counter")    -- "[data-b-counter]"
  IslandCss.attribute_for("ChatWidget") -- "[data-b-chat-widget]"
fnscope_cssscope_css(css : String, island_name : String) : String#

Scope all CSS selectors in css to the island named island_name.

Each CSS selector is prefixed with `[data-b-{kebab-name}]` so rules only
apply inside that island's DOM subtree.  Comma-separated selector lists are
each prefixed individually.  @ rules and global selectors (:root, html,
body, *) are passed through without modification.

Example:
  IslandCss.scope_css(".btn { color: red; }", "MyButton")
  -- "[data-b-my-button] .btn { color: red; }"

  IslandCss.scope_css(".a, .b { margin: 0; }", "Card")
  -- "[data-b-card] .a,\n[data-b-card] .b { margin: 0; }"
fnto_kebabto_kebab(name : String) : String#

Convert a PascalCase or CamelCase identifier to kebab-case.

Each uppercase letter that is not the first character is preceded by a "-".
Digits and already-lowercase characters are preserved.

Example:
  IslandCss.to_kebab("SearchBar")       -- "search-bar"
  IslandCss.to_kebab("MyIsland")        -- "my-island"
  IslandCss.to_kebab("Counter")         -- "counter"
  IslandCss.to_kebab("ChatWidget")      -- "chat-widget"
  IslandCss.to_kebab("UserProfileCard") -- "user-profile-card"