3.0.0 • Public • Published

    ❄️ Crystalline

    lit npm ruby2js
    bundlephobia bundlephobia dependency count bundlephobia tree shaking

    Lit: Simple. Fast. Web Components.

    Crystalline: a collection of Lit enhancements inspired by Stimulus and written in Ruby2JS. Crystalline includes:

    • DeclarativeActionsController - lets you add action attributes to elements in the light DOM as a way of providing declarative event handlers.

    • TargetsController - lets you easily query child nodes in the light DOM using either selectors or explicit attribute-based identifies. Docs coming soon!

    • CrystallineElement - a base subclass of LitElement which provides syntax benefits for Ruby2JS users as well as includes the two controllers above.

    Crystalline uses Ruby 3 and Ruby2JS to compile its source code to modern ES6+ JavaScript (example). Crystalline can be used with any modern JS bundler as well as directly in buildless HTML using script type="module".

    Crystalline works great as a spice on top of server-rendered markup originating from backend frameworks like Rails or static sites generators like Bridgetown—providing features not normally found in web component libraries that assume they're only concerned with client-rendered markup and event handling.

    Enjoy writing functional components? While I am of the opinion classes work quite well most of the time, for very simple components or components constructed out of many separate lit-html snippets, you might long for a functional shorthand. In those cases, crystallize will do just the trick!

    You can build an entire suite of reactive frontend components just with Lit/Crystalline, along with a general strategy to enhance your site with a variety of emerging web components and component libraries (Shoelace for example).


    yarn add crystalline-element


    npm i crystalline-element

    Using DeclarativeActionsController

    Demo on CodePen

    It's very simple to add this controller to any Lit 2 component. First let's set up a new test component:

    import { LitElement, html } from "lit"
    import { DeclarativeActionsController } from "crystalline-element/controllers"
    class TestElement extends LitElement {
      actions = new DeclarativeActionsController(this)
      clickMe() {
        this.shadowRoot.querySelector("test-msg").textContent = "clicked!"
      render() {
        return html`
    customElements.define("test-element", TestElement)

    You'll notice that currently nothing actually calls the clickMe method. Don't worry! We'll declaratively handle that in our regular HTML template:

        <button test-element-action="clickMe">Button</button>

    The tag name of the component (text-element) plus action sets up the event handler via an action attribute, with the method name clickMe being the value of the attribute. This is shorthand for click->clickMe. The controller defaults to click if no event type is specified (with a few exceptions, such as submit for forms and input or change for various form controls).

    Because DeclarativeActionsController uses a MutationObserver to keep an eye on HTML in the light DOM, at any time you can update the markup dynamically and actions will work as expected.

    In addition, actions don't pass component boundaries. In other words, if you were to add a test-element inside of another test-element, the action within the nested test-element would only call the method for that nested component.

    Note: actions are only detected within light DOM and do not traverse shadow trees of child components.

    Using CrystallineElement

    Demo on CodePen

    CrystallineElement is very easy to use. Simply import it, along with helpers from Lit directly, and you can start writing new web components.

    More documentation coming soon…

    Ruby Example

    import [ CrystallineElement, crystallize ], from: ""
    import [ html, css ], from: ""
    class MyComponent < CrystallineElement
      property :name, String
      stylesheet css <<~CSS
        p {
          font-weight: bold;
      define "my-component" # always add below properties, stylesheets, etc.
      def render()
        html "<p>Hello World! Nice to meet you, #{}</p>"
    class LightDomOnlyComponent < CrystallineElement
      define "light-dom-only", shadow_dom: false
      # ...
    localVariable = "functional"
      properties: {
        greeting: { type: String }
    ) do |comp|
      html <<~HTML
        <p>#{comp.greeting}, you can write "#{localVariable}" components with a handy shorthand!</p>

    JavaScript Example

    import { CrystallineElement, crystallize } from ""
    import { html, css } from ""
    class MyComponent extends CrystallineElement {
      static get properties() {
        return {
          name: { type: String }
      static get styles() {
        return css`
          p {
            font-weight: bold;
      render() {
        return html`<p>Hello World! Nice to meet you, ${}</p>`
    class LightDomOnlyComponent extends CrystallineElement {
      // ...
    LightDomComponent.define("light-dom-only", { shadowDom: false })
    const localVariable = "functional"
    crystallize("functional-component", {
      properties: {
        greeting: { type: String }
    }, comp => html`
      <p>${comp.greeting}, you can write "${localVariable}" components with a handy shorthand!</p>

    Building Source with Ruby2JS

    Requires Ruby 3.0. A Ruby version manager like rbenv is recommended. Run bundle install to set up the Ruby gems.

    Run yarn build (which gets run by the test and release script automatically) to transpile the Ruby src files to the JS dist folder.


    Crystalline uses the Modern Web Test Runner and helpers from Open WC for its test suite.

    Run yarn test to run the test suite.


    1. Fork it (
    2. Create your feature branch (git checkout -b my-new-feature)
    3. Commit your changes (git commit -am 'Add some feature')
    4. Push to the branch (git push origin my-new-feature)
    5. Create a new Pull Request






    npm i crystalline-element

    DownloadsWeekly Downloads






    Unpacked Size

    17 kB

    Total Files


    Last publish


    • jaredwhite