Search...Search plugins and themes...
⌘K
Sign in
  • Get started
  • Download
  • Pricing
  • Enterprise
  • Account
  • Obsidian
  • Overview
  • Sync
  • Publish
  • Canvas
  • Mobile
  • Web Clipper
  • CLI
  • Learn
  • Help
  • Developers
  • Changelog
  • About
  • Roadmap
  • Blog
  • Resources
  • System status
  • License overview
  • Terms of service
  • Privacy policy
  • Security
  • Community
  • Plugins
  • Themes
  • Discord
  • Forum / 中文论坛
  • Merch store
  • Brand guidelines
Follow us
DiscordTwitterBlueskyThreadsMastodonYouTubeGitHub
© 2026 Obsidian

Freeform

tmcwtmcw969 downloads

Create visualizations, run custom JS, and mix live programs with your notes

Add to Obsidian
  • Overview
  • Scorecard
  • Updates4

Obsidian freeform plugin. This lets you write arbitrary JavaScript, including importing ESM modules, injecting styles, and much more.

Inspired by Observable

This brings a taste of Observable to Obsidian. Some common elements include:

  • You can write blocks of code which run in a light sandboxed environment
  • You can use import statements to import modules from URLs, using services like esm.sh or jsdelivr.
  • export statements are supported too, but have no effect. require() is not supported. JSX is also not supported, yet.
  • There's a display() function to show values and elements, just like Observable Framework's explicit display system.

Based on iframes

Everything you write is run within a sandboxed iframe, making it safer to do more creative coding within Obsidian without affecting the surrounding page.

Get started

Install the freeform plugin, and create a fenced code block with the language set to freeform. For example:

```freeform
display(42);
```

That should, once you're done editing it and have clicked away from the code block, show the number 42. Now you've learned all of the concepts in freeform! It's JavaScript, and you can use the display() method to show a value. See the rest of this readme for some examples.

Demo

https://github.com/tmcw/obsidian-freeform/assets/32314/56b4e23a-2837-4a06-84c7-ee35b09c2634

Examples

Using Observable Plot

You can easily import Observable Plot and use it with Freeform.

```freeform
import *  as Plot from "https://cdn.jsdelivr.net/npm/@observablehq/[email protected]/+esm";
const widths = [
  ["Val Town", 1024],
  ["Val Town (after)", 900],
  ["GitHub", 830],
  ["Radicle", 723],
  ["Replicate", 653],
  ["Glitch", 612],
  ["GitLab", 842],
  ["Observable", 640]
].map(([name, width]) => ({ name, width }))

display(Plot.barX(widths, {
  x: "width",
  y: "name",
  marginLeft: 100,
  fill: "name"
}).plot({ height: 400, width }))
```

Tracking habits

Use this to track your habits in a calendar interface! It makes a few assumptions that you'll want to tweak and are noted in the code!

This assumes that you're tracking habits as boolean properties in your daily notes.

import * as d3 from "https://esm.run/d3@7";
import * as Plot from "https://esm.run/@observablehq/[email protected]";

// NOTE:
// I store my daily note in a folder called "01 Daily."
// Adjust that. Also adjust habit-scale and habit-gym to whatever
// the properties you track your habits are.
const items = await window.top.app.plugins.plugins.dataview.api
  .query(`table habit-scale, habit-gym
      from "01 Daily"`);

const parsed = items.value.values.map(row => {
  // Note: this assumes that your daily note file format is parseable
  // as dates. Mine are stored like 2024-09-24. Adjust if yours are different!
    const date = new Date(row[0].path.split('/')[1].replace('.md', ''));
    return {
        Date: date,
        habits: row.slice(1).filter(Boolean).length
    };
});

document.body.appendChild(Plot.plot({
  padding: 0,
  x: {axis: null},
  y: {tickFormat: Plot.formatWeekday("en", "narrow"), tickSize: 0},
  fx: {tickFormat: ""},
  color: {scheme: "Blues", legend: true, label: "Habit completion"},
  marks: [
    Plot.cell(parsed, {
      y: (d) => d3.utcWeek.count(d3.utcYear(d.Date), d.Date),
      x: (d) => d.Date.getUTCDay(),
      fx: (d) => d.Date.getUTCFullYear(),
      fill: (d, i) => d.habits,
      stroke: d3.interpolateBlues(0.1),
      title: (d, i) => d.habits,
      inset: 2
    })
  ]
}))

Importing a module from esm.sh

Most npm modules that are compatible with browsers are available from https://esm.sh/, jsdelivr, unpkg, or skypack. Observable Plot is an especially tricky one, but most "just work."

```freeform
import { min } from "https://esm.sh/simple-statistics";
display(min([1, 2, 3]))
```

Running Preact

Freeform doesn't support JSX syntax (yet) but frameworks like Preact can work without it.

```freeform
import { h, render } from 'https://esm.sh/preact';
// Create your app
const app = h('h1', null, 'Hello World!');
render(app, document.body);
```

Querying DataView

DataView is accessible via window.top.app.plugins.plugins.dataview.api. Here's an example of using it:

```freeform
import * as Plot from "https://cdn.jsdelivr.net/npm/@observablehq/[email protected]/+esm";

const items = await window.top.app.plugins.plugins.dataview.api
  .query(`table price, purchased, color
from "03 Stuff"
where price and sold = undefined
sort purchased desc`);

const mapped = items.value.values
  .map((item) => {
    if (!item[2]) return;
    return {
      price: item[1],
      date: new Date(item[2].toMillis()),
    };
  })
  .filter((r) => r);

display(Plot.dot(mapped, { y: "price", x: "date" }).plot());
```

If you're doing more than one thing with the DataView API, you will probably want to alias the variable, like

const dv = window.top.app.plugins.plugins.dataview.api

Also, note that Date objects that you get from DataView queries are originated from the top frame, so some code might not recognize them as Date instances. Recreating them with new Date, as in the example above, will fix that issue.

Inserting a stylesheet

Obsidian's CSP forbids <link> elements bringing in external stylesheets. You can work around this by a helper function that fetches external CSS and inlines it into a new style on the page:

async function addStyle(url) {
  const style = new CSSStyleSheet();
  style.replaceSync(await fetch(url).then(r => r.text()))
  document.adoptedStyleSheets = [...document.adoptedStyleSheets, style];
}

Note that some stylesheets you import this way will have relative references to images or they might import other stylesheets via @import, and those things won't work.

Notes

  • There is a width variable, much like Observable's, but it is not live-updating or reactive. This project does not include Observable-style reactivity: your JavaScript runs just the same way as it does on any webpage.
  • Only HTTP ESM imports are supported. This isn't Node.js or Deno - there isn't a node_modules directory, and you don't have short names for dependencies. Thankfully, this usually isn't a problem because you can use https://esm.sh/ https://www.jsdelivr.com/ and more to import modules.

Components

This plugin uses @observablehq/inspector as the display() method.

79%
HealthFair
ReviewSatisfactory
About
Run arbitrary JavaScript inside sandboxed iframes in your notes. Import ESM modules from URLs (esm.sh/jsdelivr), inject styles, and use display() to render values and DOM elements for Observable-style interactive outputs.
CodeImportHTML
Details
Current version
1.0.2
Last updated
2 years ago
Created
2 years ago
Updates
4 releases
Downloads
969
Compatible with
Obsidian 0.0.0+
Platforms
Desktop, Mobile
License
MIT
Report bugRequest featureReport plugin
Author
tmcwtmcw
github.com/tmcw
GitHubtmcw
  1. Community
  2. Plugins
  3. Code
  4. Freeform

Related plugins

Templater

Create and use dynamic templates.

Importer

Import data from Notion, Evernote, Apple Notes, Microsoft OneNote, Google Keep, Bear, Roam, and HTML files.

Mermaid Tools

Improved Mermaid.js experience: visual toolbar with common elements and more.

Zotero Integration

Insert and import citations, bibliographies, notes, and PDF annotations from Zotero.

Readwise Official

Sync highlights from Readwise to your vault.

PlantUML

Generate PlantUML diagrams.

Weread

Sync Tencent Weread highlights and annotations.

ZotLit

Integrate with Zotero, create literature notes, and insert citations from a Zotero library.

Kindle Highlights

Sync your Kindle book highlights using your Amazon login or uploading your My Clippings file.

Local Images Plus

A reincarnation of Local Images to download images in Markdown notes to local storage.