The HTML framework for shipping MVPs

REALM turns a <custom-element> tag into a working web component in the browser. Write the script, style and markup in plain HTML, load REALM from a CDN and open the file. There is no build step.

Write a web component in plain HTML

A REALM component is one <custom-element> tag that holds a script, a style and markup. Values appear through native <slot> tags and events use plain onclick. The code on the left is the whole page.

Clicked Reset
index.htmlclick-counter.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>REALM demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>

  <import-element from="./click-counter.html"></import-element>

  <click-counter></click-counter>
  <click-counter step="5"></click-counter>
</body>
</html>
<custom-element name="click-counter">
  <script>
    const props = defineProps({ step: prop.number().int().min(1).default(1) });

    const counter = useState({
      count: 0,
      get unit() {
        return this.count === 1 ? "time" : "times";
      },
    });

    function increment() {
      counter.count += props.step;
    }

    function reset() {
      counter.count = 0;
    }
  </script>

  <style>
    :host {
      display: inline-flex;
      gap: calc(var(--spacing) * 2);
    }
  </style>

  <ui-button onclick="increment()">Clicked <slot name="#counter.count"></slot> <slot name="#counter.unit"></slot></ui-button>
  <ui-button variant="outline" onclick="reset()">Reset</ui-button>
</custom-element>

State, lists, data and forms

useState returns a plain object. Change it and only the slots and attributes that read it update. Requests, forms and component files work through HTML attributes and tags.

State Fetch Forms Files

repeat renders one row per item. Getters such as left recompute when the data they read changes.

Add
  • Remove
index.htmltodo-list.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>REALM demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/checkbox.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>

  <import-element from="./todo-list.html"></import-element>

  <todo-list></todo-list>
</body>
</html>
<custom-element name="todo-list">
  <script>
    const todos = useState({
      items: [
        { id: 1, text: "Write a component", done: true },
        { id: 2, text: "Ship the MVP", done: false },
      ],
      draft: "",
      get left() {
        return `${this.items.filter((item) => !item.done).length} left`;
      },
    });

    function add(event) {
      event.preventDefault();
      const text = todos.draft.trim();
      if (!text) return;
      todos.items.push({ id: Date.now(), text, done: false });
      todos.draft = "";
    }

    function updateDraft(field) {
      todos.draft = field.value;
    }

    function toggle(item) {
      item.done = !item.done;
    }

    function remove(item) {
      todos.items.splice(todos.items.indexOf(item), 1);
    }
  </script>

  <style>
    :host {
      display: grid;
      gap: calc(var(--spacing) * 3);
      width: min(calc(var(--spacing) * 96), 100%);
    }
    form {
      display: flex;
      gap: calc(var(--spacing) * 2);
    }
    form ui-input {
      flex: 1;
    }
    ul {
      margin: 0;
      padding: 0;
      list-style: none;
    }
    li {
      display: flex;
      align-items: center;
      gap: calc(var(--spacing) * 3);
      padding: calc(var(--spacing) * 1) 0;
    }
    li span {
      flex: 1;
    }
    li[data-done] span {
      color: var(--muted-foreground);
      text-decoration: line-through;
    }
    small {
      color: var(--muted-foreground);
    }
  </style>

  <form onsubmit="add(event)">
    <ui-input aria-label="New todo" placeholder="What needs doing?" _value="#todos.draft" oninput="updateDraft(this)"></ui-input>
    <ui-button type="submit">Add</ui-button>
  </form>
  <ul>
    <li repeat="#todos.items" as="item" key="id" _data-done="#item.done">
      <ui-checkbox _aria-label="#item.text" _checked="#item.done" onchange="toggle(item)"></ui-checkbox>
      <span><slot name="#item.text"></slot></span>
      <ui-button variant="ghost" size="sm" onclick="remove(item)">Remove</ui-button>
    </li>
  </ul>
  <small><slot name="#todos.left"></slot></small>
</custom-element>

fetch names the URL, into names the state that receives the JSON and busy tracks loading. The script only declares the state.

Load books
  • by
index.htmlbook-shelf.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>REALM demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/spinner.html"></import-element>

  <import-element from="./book-shelf.html"></import-element>

  <book-shelf></book-shelf>
</body>
</html>
<custom-element name="book-shelf">
  <script>
    const shelf = useState({ books: [], loading: false });
  </script>

  <style>
    :host {
      display: grid;
      gap: calc(var(--spacing) * 3);
      justify-items: start;
      width: min(calc(var(--spacing) * 96), 100%);
    }
    ul {
      margin: 0;
      padding-left: calc(var(--spacing) * 5);
    }
    span {
      color: var(--muted-foreground);
    }
  </style>

  <ui-button fetch="http://realm.js.org/demos/books.json" into="#shelf.books" busy="#shelf.loading" _disabled="#shelf.loading">Load books</ui-button>
  <is-visible when="#shelf.loading"><ui-spinner></ui-spinner></is-visible>
  <ul>
    <li repeat="#shelf.books" as="book" key="id"><slot name="#book.title"></slot> <span>by <slot name="#book.author"></slot></span></li>
  </ul>
</custom-element>

REALM components are form fields. They submit, reset and validate with their <form>, so FormData is all you need.

Email Send me product updates Sign up
index.htmlsignup-form.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>REALM demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/field.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/label.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/select.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/switch.html"></import-element>

  <import-element from="./signup-form.html"></import-element>

  <signup-form></signup-form>
</body>
</html>
<custom-element name="signup-form">
  <script>
    const result = useState({ text: "Submit the form to see what it sends." });

    function submit(event) {
      event.preventDefault();
      const data = Object.fromEntries(new FormData(event.target));
      result.text = JSON.stringify(data, null, 2);
    }
  </script>

  <style>
    :host {
      display: grid;
      gap: calc(var(--spacing) * 4);
      width: min(calc(var(--spacing) * 96), 100%);
    }
    form {
      display: grid;
      gap: calc(var(--spacing) * 4);
    }
    pre {
      margin: 0;
      padding: calc(var(--spacing) * 3);
      border-radius: var(--radius-md);
      background: var(--muted);
      font-size: var(--text-sm);
    }
  </style>

  <form onsubmit="submit(event)">
    <ui-field>
      <ui-label for="signup-email">Email</ui-label>
      <ui-input id="signup-email" name="email" type="email" required placeholder="you@example.com"></ui-input>
    </ui-field>
    <ui-select name="plan" aria-label="Plan">
      <option value="free">Free</option>
      <option value="team">Team</option>
    </ui-select>
    <ui-switch name="updates" checked>Send me product updates</ui-switch>
    <ui-button type="submit">Sign up</ui-button>
  </form>
  <pre><slot name="#result.text"></slot></pre>
</custom-element>

Keep each component in its own HTML file and load it with <import-element>. The file is fetched once and parsed, never run as a script. Open the profile-card.html tab to see it.

index.htmlprofile-card.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>REALM demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
  <style>
    .demo-narrow {
      width: min(100%, calc(var(--spacing) * 88));
    }
    .demo-stack {
      display: grid;
      gap: calc(var(--spacing) * 4);
    }
  </style>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/avatar.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/card.html"></import-element>

  <import-element from="./profile-card.html"></import-element>

  <div class="demo-narrow demo-stack">
    <profile-card name="Ada Lovelace" job="Mathematician"></profile-card>
    <profile-card name="Alan Turing" job="Computer scientist"></profile-card>
  </div>
</body>
</html>
<custom-element name="profile-card">
  <script>
    const props = defineProps({
      name: prop.string().default("Anonymous"),
      job: prop.string().default(""),
    });

    const profile = useState({
      following: false,
      get initials() {
        return props.name
          .split(" ")
          .map((word) => word[0])
          .join("");
      },
      get label() {
        return this.following ? "Following" : "Follow";
      },
      get variant() {
        return this.following ? "outline" : "default";
      },
    });

    function toggle() {
      profile.following = !profile.following;
    }
  </script>

  <style>
    :host {
      display: block;
    }
    .profile {
      display: flex;
      align-items: center;
      gap: calc(var(--spacing) * 3);
    }
    .name {
      display: grid;
      flex: 1;
      min-width: 0;
      font-size: var(--text-sm);
    }
    .job {
      color: var(--muted-foreground);
    }
  </style>

  <ui-card>
    <ui-card-content class="profile">
      <ui-avatar><ui-avatar-fallback><slot name="#profile.initials"></slot></ui-avatar-fallback></ui-avatar>
      <span class="name">
        <strong><slot name="@name"></slot></strong>
        <span class="job"><slot name="@job"></slot></span>
      </span>
      <ui-button size="sm" _variant="#profile.variant" _aria-pressed="#profile.following" onclick="toggle()"><slot name="#profile.label"></slot></ui-button>
    </ui-card-content>
  </ui-card>
</custom-element>

Server-rendered HTML that search engines can read

Your server sends the page as ordinary HTML, so titles, text, links and meta tags are all in the first response. Search engines and AI crawlers index it without running JavaScript, and REALM upgrades the custom tags into components in the browser. There is no hydration step and no JavaScript server to run.

Your server template
faq.php faq.html.erb faq.html faq.tmpl
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/accordion.html"></import-element>

<ui-accordion>
  <?php foreach ($faqs as $faq): ?>
    <ui-accordion-item>
      <ui-accordion-trigger><?= htmlspecialchars($faq->question) ?></ui-accordion-trigger>
      <ui-accordion-content><?= htmlspecialchars($faq->answer) ?></ui-accordion-content>
    </ui-accordion-item>
  <?php endforeach ?>
</ui-accordion>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/accordion.html"></import-element>

<ui-accordion>
  <% @faqs.each do |faq| %>
    <ui-accordion-item>
      <ui-accordion-trigger><%= faq.question %></ui-accordion-trigger>
      <ui-accordion-content><%= faq.answer %></ui-accordion-content>
    </ui-accordion-item>
  <% end %>
</ui-accordion>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/accordion.html"></import-element>

<ui-accordion>
  {% for faq in faqs %}
    <ui-accordion-item>
      <ui-accordion-trigger>{{ faq.question }}</ui-accordion-trigger>
      <ui-accordion-content>{{ faq.answer }}</ui-accordion-content>
    </ui-accordion-item>
  {% endfor %}
</ui-accordion>
<import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/accordion.html"></import-element>

<ui-accordion>
  {{range .FAQs}}
    <ui-accordion-item>
      <ui-accordion-trigger>{{.Question}}</ui-accordion-trigger>
      <ui-accordion-content>{{.Answer}}</ui-accordion-content>
    </ui-accordion-item>
  {{end}}
</ui-accordion>
What the browser shows
Previewindex.html
Does REALM need a build step? No. Load it from a CDN and write each component in an HTML file. Can search engines read these answers? Yes. Every question and answer is text in the page's HTML. The accordion only adds opening and closing. Which servers does it work with? Any server that sends HTML: PHP, Rails, Django, Go, Node.js or a static site generator.
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>REALM demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
  <style>
    .demo-medium {
      width: min(100%, calc(var(--spacing) * 120));
    }
  </style>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/accordion.html"></import-element>

  <div class="demo-medium">
    <ui-accordion>
      <ui-accordion-item open>
        <ui-accordion-trigger>Does REALM need a build step?</ui-accordion-trigger>
        <ui-accordion-content>No. Load it from a CDN and write each component in an HTML file.</ui-accordion-content>
      </ui-accordion-item>
      <ui-accordion-item>
        <ui-accordion-trigger>Can search engines read these answers?</ui-accordion-trigger>
        <ui-accordion-content>Yes. Every question and answer is text in the page's HTML. The accordion only adds opening and closing.</ui-accordion-content>
      </ui-accordion-item>
      <ui-accordion-item>
        <ui-accordion-trigger>Which servers does it work with?</ui-accordion-trigger>
        <ui-accordion-content>Any server that sends HTML: PHP, Rails, Django, Go, Node.js or a static site generator.</ui-accordion-content>
      </ui-accordion-item>
    </ui-accordion>
  </div>
</body>
</html>
What a crawler receives
<head>
  <title>FAQ · Acme</title>
  <meta name="description" content="Answers about Acme.">
</head>
<body>
  <ui-accordion>
    <ui-accordion-item>
      <ui-accordion-trigger>Does REALM need a build step?</ui-accordion-trigger>
      <ui-accordion-content>No. Load it from a CDN and write each component in an HTML file.</ui-accordion-content>
    </ui-accordion-item>
    <ui-accordion-item>
      <ui-accordion-trigger>Can search engines read these answers?</ui-accordion-trigger>
      <ui-accordion-content>Yes. Every question and answer is text in the page's HTML.</ui-accordion-content>
    </ui-accordion-item>
  </ui-accordion>
</body>
  • Indexed from the first responseCompound parts hold their content as ordinary child elements, so every question and answer is in the HTML a search engine fetches.
  • Readable by AI crawlersMany AI crawlers do not run JavaScript. They read the same text, because it never depended on a script.
  • Nothing to hydrateThe server's markup is the final markup. REALM attaches behaviour to it in place, with no second render and no state payload.
  • Link previews from your serverThe title, description and Open Graph tags are server output, so shared links unfurl with the right card.

Put content in compound parts and search engines index it. What a component draws inside its own shadow root is not in the HTML, because REALM does not output Declarative Shadow DOM.

Sixty UI components and 50,939 icons

REALM UI has about sixty accessible ui-* components: buttons, forms, dialogs, menus, tabs, tables and charts. Import one file and use the tags. Dark mode and theming come from CSS variables.

Launch your MVP Every control here is a ui-* tag. Beta Project name Email me when it is live Cancel Launch
index.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>REALM demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
  <style>
    .demo-narrow {
      width: min(100%, calc(var(--spacing) * 88));
    }
    .demo-stack {
      display: grid;
      gap: calc(var(--spacing) * 4);
    }
  </style>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/badge.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/card.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/field.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/input.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/label.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/switch.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/toaster.html"></import-element>
  <ui-toaster></ui-toaster>

  <div class="demo-narrow">
    <ui-card>
      <ui-card-header>
        <ui-card-title>Launch your MVP</ui-card-title>
        <ui-card-description>Every control here is a ui-* tag.</ui-card-description>
        <ui-card-action><ui-badge variant="secondary">Beta</ui-badge></ui-card-action>
      </ui-card-header>
      <ui-card-content class="demo-stack">
        <ui-field>
          <ui-label for="launch-name">Project name</ui-label>
          <ui-input id="launch-name" placeholder="my-first-mvp"></ui-input>
        </ui-field>
        <ui-switch checked>Email me when it is live</ui-switch>
      </ui-card-content>
      <ui-card-footer justify="between">
        <ui-button variant="outline">Cancel</ui-button>
        <ui-button onclick="document.dispatchEvent(new CustomEvent('toast', { detail: { title: 'Launched', description: 'Your MVP is live.' } }))">Launch</ui-button>
      </ui-card-footer>
    </ui-card>
  </div>
</body>
</html>

Icons from 31 sets in one tag

Unicons gives you 50,939 icons through <u-icon name="heart" set="tb">. Each page downloads only the icons it uses. Browse the catalog

Built on browser features, with no dependencies

The REALM runtime is 16 KB gzipped and depends on nothing. Each UI component wraps a feature the browser already ships, so focus, layering, keyboard and forms behave like built-in elements. Try them.

Open dialog A native dialog Press Escape or click outside to close it. Focus goes back to the button. Close

<ui-dialog><dialog>

Top layer, focus moves in, Escape closes.

Open popover Click anywhere outside to close it.

<ui-popover>popover

The Popover API: no z-index fights, light dismiss.

Send

<ui-input>ElementInternals

Submit it empty and the browser checks it.

<ui-select><select>

Plain <option> tags and the native picker.

<ui-toggle-group>focusgroup

Tab in once, then arrow keys move between buttons.

Email updates

<ui-switch>:state(attr-checked)

CustomStateSet: style it like a native pseudo-class.

16 KB
runtime, gzipped
0
dependencies
100%
test coverage
  • Chrome
  • Edge
  • Firefox
  • Safari
Tested in every major browser and engine. Browser support

Build with ChatGPT, Claude or Gemini

A REALM component is one HTML file with native tags, a script and a style. There is no JSX and no compiled output for a model to reason about, and the REALM Agent Skill gives your coding assistant the rules.

  • ChatGPT
  • Claude
  • Gemini

npx skills add realmorg/realm

installs the REALM Agent Skill from the realmorg/realm repository on GitHub, or download realm.zip

Add a pricing table with REALM UI to index.html

  • One file per componentScript, style and markup sit together, so a model reads the whole component at once.
  • Native tags, nothing compiledWhat the model writes is what the browser runs. View Source shows the same code.
  • Docs as MarkdownAdd .md to any docs address. llms.txt and llms-full.txt list every page.
  • Rules with testsBehaviour is written as specs, and each rule has a test with the same name.

The kind of file that comes back. It is plain HTML and it runs right here.

Monthly Yearly
Hobby One project, forever. $0 free Start free Team Unlimited projects. Popular Start a trial
index.htmlpricing-table.html
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>REALM demo</title>
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
  <script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/badge.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/button.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/card.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/toggle-group.html"></import-element>
  <import-element from="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/components/toggle.html"></import-element>

  <import-element from="./pricing-table.html"></import-element>

  <pricing-table></pricing-table>
</body>
</html>
<custom-element name="pricing-table">
  <script>
    const pricing = useState({
      yearly: false,
      get price() {
        return this.yearly ? "$190" : "$19";
      },
      get period() {
        return this.yearly ? "per year" : "per month";
      },
    });

    function setBilling(group) {
      pricing.yearly = group.value === "yearly";
    }
  </script>

  <style>
    :host {
      display: grid;
      gap: calc(var(--spacing) * 4);
      justify-items: center;
      width: min(calc(var(--spacing) * 128), 100%);
    }
    .plans {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(calc(var(--spacing) * 52), 1fr));
      gap: calc(var(--spacing) * 4);
      width: 100%;
    }
    strong {
      font-size: var(--text-3xl);
    }
    small {
      color: var(--muted-foreground);
    }
  </style>

  <ui-toggle-group value="monthly" aria-label="Billing" onchange="setBilling(this)">
    <ui-toggle value="monthly" variant="outline">Monthly</ui-toggle>
    <ui-toggle value="yearly" variant="outline">Yearly</ui-toggle>
  </ui-toggle-group>
  <div class="plans">
    <ui-card>
      <ui-card-header>
        <ui-card-title>Hobby</ui-card-title>
        <ui-card-description>One project, forever.</ui-card-description>
      </ui-card-header>
      <ui-card-content><strong>$0</strong> <small>free</small></ui-card-content>
      <ui-card-footer><ui-button variant="outline">Start free</ui-button></ui-card-footer>
    </ui-card>
    <ui-card>
      <ui-card-header>
        <ui-card-title>Team</ui-card-title>
        <ui-card-description>Unlimited projects.</ui-card-description>
        <ui-card-action><ui-badge>Popular</ui-badge></ui-card-action>
      </ui-card-header>
      <ui-card-content><strong><slot name="#pricing.price"></slot></strong> <small><slot name="#pricing.period"></slot></small></ui-card-content>
      <ui-card-footer><ui-button>Start a trial</ui-button></ui-card-footer>
    </ui-card>
  </div>
</custom-element>

Apps built with REALM

Complete apps that run on this site. Each is a few HTML files and small JavaScript modules, so View Source shows the whole app.

All examples

Start with one HTML file

Add these two tags to any page. There is no npm install, no bundler and no config file, and View Source shows the code you wrote.

<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>