3.0.1 • Public • Published

    ❄️ Crystallized

    This package has moved to @crystallized/controllers. Please visit for details.

    lit npm ruby2js
    bundlephobia bundlephobia dependency count bundlephobia tree shaking

    Lit: Simple. Fast. Web Components.

    Crystallized: a collection of Lit 2 enhancements, starting with controllers inspired by Stimulus. Crystallized 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!

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

    Lit along with Crystallized 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.

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

    NOTE: this package is currently in the process of transitioning to @crystallized/controllers. All of the below information is deprecated. Please come back shortly for updated instructions!


    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

    10.1 kB

    Total Files


    Last publish


    • jaredwhite