Notes Without Folders (Visual Board View)

January 21, 2026

A visual board view you can drop into a note-taking app (or any page with a list of notes). Instead of burying notes in nested folders, you place them as cards on a canvas and let spatial memory + tags do the organizing.

What it does:

  • Renders your notes as draggable cards on a board (infinite-ish canvas with pan).
  • Saves card positions automatically in localStorage (per board).
  • Includes a quick tag filter (type #tag or plain text).
  • No framework, no dependencies, no build step.

Install:

  1. Paste the HTML container anywhere: <div id="noteBoard"></div>
  2. Paste the script below before </body>
  3. Replace the NOTES array with your real notes (or hydrate it from your backend).

How to use:

  • Drag cards to position them.
  • Hold Space + drag to pan the board.
  • Use the filter box to show notes by keyword or #tag.
<!-- 1) Drop-in container -->
<div id="noteBoard"></div>

<script>
(function(){
  /**
   * Visual Board Notes (No Folders) — Drop-in
   * - Draggable note cards
   * - Space+drag to pan
   * - Saves positions in localStorage
   * - Filter by text or #tag
   */

  // ========= YOUR NOTES =========
  // Required fields: id, title, body, tags[]
  var NOTES = [
    { id:"n1", title:"Client A — kickoff", body:"Discuss scope, deliverables, timelines. Next: send proposal.", tags:["work","clientA","meeting"] },
    { id:"n2", title:"Random idea", body:"A board view that replaces nested folders. Tags + spatial memory.", tags:["ideas","notes"] },
    { id:"n3", title:"Grocery", body:"Eggs, coffee, rice, hot sauce.", tags:["personal","todo"] },
    { id:"n4", title:"Bug: replies sorting", body:"Replies tab not ordering by last reply. Fix query and test.", tags:["dev","bugs"] }
  ];

  // Board identity (change per board/view)
  var BOARD_KEY = "noteBoard:default";

  // ========= UTIL =========
  function $(sel, root){ return (root||document).querySelector(sel); }
  function el(tag, attrs){
    var n = document.createElement(tag);
    if (attrs) for (var k in attrs) if (Object.prototype.hasOwnProperty.call(attrs,k)) {
      if (k === "text") n.textContent = attrs[k];
      else if (k === "html") n.innerHTML = attrs[k];
      else n.setAttribute(k, attrs[k]);
    }
    return n;
  }
  function clamp(n,min,max){ return Math.max(min, Math.min(max,n)); }
  function safeJsonParse(s, fallback){
    try { return JSON.parse(s); } catch(e){ return fallback; }
  }
  function toKey(id){ return BOARD_KEY + ":pos:" + id; }

  function loadPos(id){
    var j = safeJsonParse(localStorage.getItem(toKey(id)) || "", null);
    if (!j || typeof j.x !== "number" || typeof j.y !== "number") return null;
    return j;
  }
  function savePos(id, x, y){
    localStorage.setItem(toKey(id), JSON.stringify({x:x,y:y,t:Date.now()}));
  }

  // ========= STYLES =========
  var css = `
  :root{
    --bg:#0b0f16; --panel:#111a26; --panel2:#0c1420;
    --text:#e8eef8; --muted:rgba(232,238,248,.72);
    --line:rgba(255,255,255,.10); --accent:#2a8cff;
    --shadow:0 18px 55px rgba(0,0,0,.45);
    --r:16px;
  }
  .nb-wrap{background:var(--bg);border:1px solid var(--line);border-radius:var(--r);overflow:hidden;box-shadow:var(--shadow)}
  .nb-top{display:flex;gap:10px;flex-wrap:wrap;align-items:center;padding:12px;background:linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02))}
  .nb-title{font:900 14px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:var(--text)}
  .nb-hint{font:600 12px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:var(--muted)}
  .nb-input{
    flex:1;min-width:220px;
    padding:10px 12px;border-radius:12px;border:1px solid var(--line);
    background:var(--panel2);color:var(--text);outline:none;
    font:700 13px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
  }
  .nb-btn{
    border:0;border-radius:12px;padding:10px 12px;
    background:var(--accent);color:#06101a;font-weight:900;cursor:pointer;
  }
  .nb-stage{
    position:relative;height:70vh;min-height:520px;background:
      radial-gradient(circle at 1px 1px, rgba(255,255,255,.08) 1px, transparent 0) 0 0/22px 22px,
      linear-gradient(180deg, rgba(255,255,255,.03), rgba(255,255,255,0));
    overflow:hidden;
    cursor:grab;
  }
  .nb-stage.panning{cursor:grabbing}
  .nb-world{position:absolute;left:0;top:0;transform:translate(0,0)}
  .nb-card{
    position:absolute;width:260px;
    background:rgba(17,26,38,.92);
    border:1px solid rgba(255,255,255,.12);
    border-radius:16px;box-shadow:0 16px 50px rgba(0,0,0,.42);
    padding:12px;
    user-select:none;
  }
  .nb-card:active{transform:scale(1.01)}
  .nb-head{display:flex;justify-content:space-between;gap:10px;align-items:flex-start}
  .nb-card h3{margin:0;font:900 13px/1.2 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;color:var(--text)}
  .nb-tags{display:flex;gap:6px;flex-wrap:wrap;margin-top:8px}
  .nb-tag{
    font:800 11px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif;
    color:rgba(232,238,248,.82);
    background:rgba(42,140,255,.14);
    border:1px solid rgba(42,140,255,.25);
    padding:4px 8px;border-radius:999px;
  }
  .nb-body{margin-top:8px;color:var(--muted);font:650 12px/1.45 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
  .nb-mini{opacity:.75;font:700 11px/1 system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif}
  `;
  var style = el("style"); style.textContent = css; document.head.appendChild(style);

  // ========= BUILD UI =========
  var host = document.getElementById("noteBoard");
  if (!host) return;

  var wrap = el("div", {class:"nb-wrap"});
  var top  = el("div", {class:"nb-top"});
  var ttl  = el("div", {class:"nb-title", text:"Visual Board Notes"});
  var hint = el("div", {class:"nb-hint", text:"Drag cards • Space+drag to pan • Filter by text or #tag"});
  var input= el("input", {class:"nb-input", placeholder:"Filter… (try: #work or meeting)"});
  var reset= el("button", {class:"nb-btn", type:"button", text:"Reset layout"});
  top.appendChild(ttl);
  top.appendChild(hint);
  top.appendChild(input);
  top.appendChild(reset);

  var stage = el("div", {class:"nb-stage"});
  var world = el("div", {class:"nb-world"});
  stage.appendChild(world);

  wrap.appendChild(top);
  wrap.appendChild(stage);
  host.appendChild(wrap);

  // ========= PAN STATE =========
  var pan = {x: 0, y: 0};
  var panning = false;
  var panStart = {x:0,y:0,px:0,py:0};
  var spaceDown = false;

  function setWorldTransform(){
    world.style.transform = "translate(" + pan.x + "px," + pan.y + "px)";
  }

  window.addEventListener("keydown", function(e){
    if (e.code === "Space") spaceDown = true;
  });
  window.addEventListener("keyup", function(e){
    if (e.code === "Space") spaceDown = false;
  });

  stage.addEventListener("mousedown", function(e){
    if (!spaceDown) return;
    panning = true;
    stage.classList.add("panning");
    panStart.x = e.clientX;
    panStart.y = e.clientY;
    panStart.px = pan.x;
    panStart.py = pan.y;
    e.preventDefault();
  });

  window.addEventListener("mousemove", function(e){
    if (!panning) return;
    pan.x = panStart.px + (e.clientX - panStart.x);
    pan.y = panStart.py + (e.clientY - panStart.y);
    setWorldTransform();
  });

  window.addEventListener("mouseup", function(){
    if (!panning) return;
    panning = false;
    stage.classList.remove("panning");
  });

  // ========= CARD RENDER =========
  var cards = [];
  function defaultLayout(i){
    // neat staggered grid
    var col = i % 4;
    var row = Math.floor(i / 4);
    return { x: 40 + col * 290, y: 40 + row * 210 };
  }

  function makeCard(note, i){
    var c = el("div", {class:"nb-card"});
    c.setAttribute("data-id", note.id);

    var head = el("div", {class:"nb-head"});
    var h3 = el("h3"); h3.textContent = note.title || "Untitled";
    var mini = el("div", {class:"nb-mini", text: (note.tags && note.tags.length) ? "#" + note.tags[0] : ""});
    head.appendChild(h3);
    head.appendChild(mini);

    var body = el("div", {class:"nb-body"});
    body.textContent = (note.body || "").slice(0, 200);

    var tags = el("div", {class:"nb-tags"});
    (note.tags || []).slice(0, 8).forEach(function(t){
      tags.appendChild(el("span", {class:"nb-tag", text:"#"+t}));
    });

    c.appendChild(head);
    c.appendChild(body);
    c.appendChild(tags);

    // position
    var pos = loadPos(note.id) || defaultLayout(i);
    c.style.left = pos.x + "px";
    c.style.top  = pos.y + "px";

    // drag behavior
    var dragging = false;
    var start = {x:0,y:0,ox:0,oy:0};

    c.addEventListener("mousedown", function(e){
      if (spaceDown) return; // let stage pan
      dragging = true;
      start.x = e.clientX;
      start.y = e.clientY;
      start.ox = parseFloat(c.style.left) || 0;
      start.oy = parseFloat(c.style.top)  || 0;
      c.style.zIndex = String(1000 + i);
      e.preventDefault();
    });

    window.addEventListener("mousemove", function(e){
      if (!dragging) return;
      var nx = start.ox + (e.clientX - start.x);
      var ny = start.oy + (e.clientY - start.y);
      c.style.left = nx + "px";
      c.style.top  = ny + "px";
    });

    window.addEventListener("mouseup", function(){
      if (!dragging) return;
      dragging = false;
      var nx = parseFloat(c.style.left) || 0;
      var ny = parseFloat(c.style.top)  || 0;
      savePos(note.id, nx, ny);
    });

    return c;
  }

  function render(list){
    world.innerHTML = "";
    cards = [];
    for (var i=0;i<list.length;i++){
      var c = makeCard(list[i], i);
      cards.push(c);
      world.appendChild(c);
    }
    setWorldTransform();
  }

  // ========= FILTER =========
  function matches(note, q){
    q = (q || "").trim().toLowerCase();
    if (!q) return true;

    // tag filter: "#work"
    if (q[0] === "#") {
      var t = q.slice(1);
      return (note.tags || []).some(function(x){ return String(x).toLowerCase() === t; });
    }

    // text match
    var hay = (note.title||"") + " " + (note.body||"") + " " + (note.tags||[]).join(" ");
    return hay.toLowerCase().indexOf(q) !== -1;
  }

  input.addEventListener("input", function(){
    var q = input.value;
    var filtered = NOTES.filter(function(n){ return matches(n, q); });
    render(filtered);
  });

  // reset positions
  reset.addEventListener("click", function(){
    if (!confirm("Reset saved card positions for this board?")) return;
    NOTES.forEach(function(n){ localStorage.removeItem(toKey(n.id)); });
    pan.x = 0; pan.y = 0;
    input.value = "";
    render(NOTES);
  });

  // init
  render(NOTES);
})();
</script>

Comments (0)

No comments yet — be the first.

← Back to all scripts