Dojo NG is being built in the open, one section at a time — follow along on Heptapod.

GUIDE

Getting started

Install Dojo NG and render your first component, in your framework of choice.

  1. Install

    npm install @dojo-ng/button
  2. Import the component

    import '@dojo-ng/button';
  3. Render it

    function App() {
      return <dj-button kind="contained">Get started</dj-button>;
    }

Working with React

Use React 19 or later. React 19 passes arrays and objects to a custom element as properties, so <dj-select options={plans}> works as written. Earlier versions turn every prop into a string attribute.

For a custom event, write on followed by its exact name:

<dj-chip closeable ondj-close={() => setLabel("(cleared)")}>{label}</dj-chip>

In TypeScript, React's JSX types do not know the dj-* tags until you declare them once:

// dj-elements.d.ts — anywhere in your TypeScript project
import "react";

declare module "react" {
  namespace JSX {
    interface IntrinsicElements {
      [tag: `dj-${string}`]: any;
    }
  }
}

Import the packages before the first render. The React page explains why, and covers forms and React 18.

  1. Install

    npm install @dojo-ng/button
  2. Import the component

    // main.js
    import '@dojo-ng/button';
  3. Render it

    <template>
      <dj-button kind="contained">Get started</dj-button>
    </template>

Working with Vue

Tell Vue's template compiler that dj-* tags are custom elements, so it stops looking for Vue components by those names:

// vite.config.js
import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: { isCustomElement: (tag) => tag.startsWith("dj-") },
      },
    }),
  ],
});

Add .prop to make sure an array or object is set as a property, and bind custom events with @:

<dj-select label="Plan" :options.prop="plans"></dj-select>
<dj-chip closeable @dj-close="label = '(cleared)'">{{ label }}</dj-chip>

The Vue page covers v-model and forms.

  1. Install

    npm install @dojo-ng/button
  2. Import the component

    // main.ts
    import '@dojo-ng/button';
  3. Render it

    import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
    
    @Component({
      selector: 'app-root',
      template: `<dj-button kind="contained">Get started</dj-button>`,
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
    })
    export class AppComponent {}

Working with Angular

CUSTOM_ELEMENTS_SCHEMA goes on each standalone component, or NgModule, whose templates use a dj-* tag. Angular sets bracket bindings as DOM properties, so arrays and objects pass straight through, and custom events bind with parentheses:

<dj-select label="Plan" [options]="plans"></dj-select>
<dj-chip closeable (dj-close)="label = '(cleared)'">{{ label }}</dj-chip>

The Angular page covers ngModel and reactive forms.

  1. Install

    npm install @dojo-ng/button
  2. Import the component

    <!-- App.svelte -->
    <script>
      import '@dojo-ng/button';
    </script>
  3. Render it

    <dj-button kind="contained">Get started</dj-button>

Working with Svelte

Svelte needs no configuration. It sets a prop as a property whenever the element defines one, so arrays and objects pass straight through. Custom events bind with on followed by the event name:

<dj-select label="Plan" options={plans}></dj-select>
<dj-chip closeable ondj-close={() => (label = '(cleared)')}>{label}</dj-chip>

The Svelte page covers state and forms.

  1. Add an import map

    <script type="importmap">
    {
      "imports": {
        "@dojo-ng/button": "https://esm.sh/@dojo-ng/button"
      }
    }
    </script>
  2. Import the component

    <script type="module">import "@dojo-ng/button";</script>
  3. Render it

    <dj-button kind="contained">Get started</dj-button>

Working without a framework

No install and no build step. esm.sh serves each package as an ES module and resolves its dependencies, Lit among them. Add one import map entry per package you use.

Set array and object values as properties from script, and listen for events the usual way:

<dj-select id="plan" label="Plan"></dj-select>

<script type="module">
  import "@dojo-ng/select";

  const plan = document.getElementById("plan");
  plan.options = [
    { value: "free", label: "Free" },
    { value: "pro", label: "Pro" },
  ];
  plan.addEventListener("change", () => console.log(plan.value));
</script>

The Plain HTML page covers bundlers, version pinning, and events.

Load the theme

Every component carries fallback values, so it renders without a theme. @dojo-ng/theme adds the full --dj-* token set and a dark mode that follows the operating system, or is fixed with data-dj-theme="dark" on <html>. Link it, or add @dojo-ng/theme/theme.css to your bundler's global styles. The theming guide covers the rest.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@dojo-ng/theme/theme.css">

Supporting Safari before 16.4

Form controls such as dj-text-input rely on ElementInternals, which Safari added in 16.4. To support Safari 15 through 16.3, load element-internals-polyfill before the first dj-* import. It does nothing in browsers that already have the API.

import "element-internals-polyfill";
import "@dojo-ng/text-input";
Next: Concepts The model behind every dj-* component.