Getting clickable titles in dataview

What I’m trying to do

Hey guys. I am trying to create a dataview of my book notes in Obsidian (I tried using projects (plugin), and it works but it doesn’t look as good as I would like

Here’s the code I am using, in a dataviewjs box


function valueMatches(input, target) {
  if (Array.isArray(input)) {
    return input.map(String).includes(String(target));
  }
  return String(input) === String(target);
}

// Get books
const booksRead2025 = dv.pages().where(book =>
  valueMatches(book.year, 2025) &&
  valueMatches(book.status, "Read")
);
const booksTBR = dv.pages().where(book =>
  valueMatches(book.status, "TBR")
);
const booksWishlist = dv.pages().where(book =>
  valueMatches(book.status, "Wishlist")
);
const booksReadAll = dv.pages().where(book =>
  valueMatches(book.status, "Read")
);

// Create wrapper
const wrapper = dv.el("div", "", {
  style: "position: relative; text-align: center;"
});

// Button container
const buttonContainer = document.createElement("div");
buttonContainer.style.cssText = `
  margin-bottom: 12px;
  display: flex;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
`;

const buttons = {
  read2025: createButton("📚 2025 Books", "read2025"),
  tbr: createButton("📝 TBR", "tbr"),
  wishlist: createButton("📌 Wishlist", "wishlist"),
  readAll: createButton("✅ Read (All)", "readAll")
};

Object.values(buttons).forEach(btn => buttonContainer.appendChild(btn));
wrapper.appendChild(buttonContainer);

// Function to create button
function createButton(label, section) {
  const btn = document.createElement("button");
  btn.innerText = label;
  btn.style.cssText = `
    padding: 6px 12px;
    border-radius: 5px;
    cursor: pointer;
    font-weight: bold;
  `;
  btn.onclick = () => toggleSection(section);
  return btn;
}

// Function to get year color
function getYearColor(year) {
  const pastelColors = [
    "#FFD1DC", "#C1E1C1", "#FFECB3", "#B3E5FC", "#D1C4E9",
    "#F8BBD0", "#C8E6C9", "#FFF9C4", "#B2DFDB", "#E1BEE7"
  ];
  const index = parseInt(year) % pastelColors.length;
  return pastelColors[index];
}

// Function to render books
function renderBooks(books, showYear = false) {
  return books.map(book => {
    const yearBadge = showYear && book.year ? `
      <div style="
        background-color: ${getYearColor(book.year)};
        display: inline-block;
        padding: 2px 8px;
        margin-top: 4px;
        font-size: 0.75em;
        border-radius: 12px;
        color: #333;
      ">
        ${book.year}
      </div>` : "";

    return `
      <div style="
        display: inline-block;
        width: 18%;
        min-width: 140px;
        max-width: 180px;
        margin: 1%;
        vertical-align: top;
        text-align: center;
      ">
        <a href="${book.file.path}">
          <img src="${book.cover}" style="height: 150px; margin: 0 auto; display: block; border-radius: 6px;" />
        </a>
        <div style="font-weight: bold; margin-top: 6px;">
          <a href="${book.file.path}" style="text-decoration: none; color: inherit;">
            ${book.file.name}
          </a>
        </div>
        <div style="font-size: 0.85em;">${book.pages ?? ""}</div>
        <div style="font-size: 0.85em; color: gray;">${book.author ?? ""}</div>
        ${yearBadge}
      </div>
    `;
  }).join("");
}

// Content container
const container = document.createElement("div");
container.style.cssText = `
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.5s ease;
  text-align: center;
  width: 100%;
  padding: 0 10px;
`;
wrapper.appendChild(container);

// Toggle logic
let currentExpanded = null;

function toggleSection(section) {
  let books;
  let showYear = false;

  if (section === "read2025") books = booksRead2025;
  else if (section === "tbr") books = booksTBR;
  else if (section === "wishlist") books = booksWishlist;
  else if (section === "readAll") {
    books = booksReadAll;
    showYear = true;
  }

  if (currentExpanded === section) {
    container.style.maxHeight = "0";
    currentExpanded = null;
  } else {
    container.innerHTML = renderBooks(books, showYear);
    container.style.maxHeight = container.scrollHeight + "px";
    currentExpanded = section;
  }
}

It’s got the look that I want, but the titles of the books are no longer clickable. I’d like to be able to click on the title or cover and get to the note of that book. Can anyone help?

Things I have tried

I tinkered with dv.fileLink() a ton… Sometimes I got the links back but they were separated from the book card (see below) I also tried to create a property called notelink that contains a link to the specific note and display that under the author

#this code split the linked titles from the cards

function valueMatches(input, target) {
  if (Array.isArray(input)) {
    return input.map(String).includes(String(target));
  }
  return String(input) === String(target);
}

// Book queries
const booksRead2025 = dv.pages().where(book =>
  valueMatches(book.year, 2025) && valueMatches(book.status, "Read")
);
const booksTBR = dv.pages().where(book =>
  valueMatches(book.status, "TBR")
);
const booksWishlist = dv.pages().where(book =>
  valueMatches(book.status, "Wishlist")
);
const booksReadAll = dv.pages().where(book =>
  valueMatches(book.status, "Read")
);

// Main wrapper
const wrapper = dv.el("div", "", { style: "position: relative; text-align: center;" });

// Button row
const buttonContainer = document.createElement("div");
buttonContainer.style.cssText = `
  margin-bottom: 12px;
  display: flex;
  justify-content: center;
  gap: 10px;
  flex-wrap: wrap;
`;

// Buttons
const buttons = [
  { label: "📚 2025 Books", id: "read2025" },
  { label: "📝 TBR", id: "tbr" },
  { label: "📌 Wishlist", id: "wishlist" },
  { label: "✅ Read (All)", id: "readAll" }
];

buttons.forEach(({ label, id }) => {
  const btn = document.createElement("button");
  btn.innerText = label;
  btn.style.cssText = `
    padding: 6px 12px;
    border-radius: 5px;
    cursor: pointer;
  `;
  btn.onclick = () => toggleSection(id);
  buttonContainer.appendChild(btn);
});

wrapper.appendChild(buttonContainer);

// Year badge color
function getYearColor(year) {
  const pastelColors = [
    "#FFD1DC", "#C1E1C1", "#FFECB3", "#B3E5FC", "#D1C4E9",
    "#F8BBD0", "#C8E6C9", "#FFF9C4", "#B2DFDB", "#E1BEE7"
  ];
  const index = parseInt(year) % pastelColors.length;
  return pastelColors[index];
}

// Main display area
const container = document.createElement("div");
container.style.cssText = `
  max-height: 0;
  overflow: hidden;
  transition: max-height 0.5s ease;
  text-align: center;
  width: 100%;
  padding: 0 10px;
`;
wrapper.appendChild(container);

// Render function (with dv.el() for the clickable title)
function renderBooks(books, showYear = false) {
  container.innerHTML = ""; // clear existing content

  books.forEach(book => {
    const card = document.createElement("div");
    card.style.cssText = `
      display: inline-block;
      width: 18%;
      min-width: 140px;
      max-width: 180px;
      margin: 1%;
      vertical-align: top;
      text-align: center;
    `;

    const img = document.createElement("img");
    img.src = book.cover;
    img.style.cssText = "height: 150px; margin: 0 auto; display: block; border-radius: 6px;";
    card.appendChild(img);

    const title = document.createElement("div");
    title.style.cssText = "font-weight: bold; margin-top: 6px;";
    // Use Dataview to render the link properly
    dv.el("span", book.file.link, {}, title);
    card.appendChild(title);

    if (book.pages) {
      const pages = document.createElement("div");
      pages.innerText = book.pages;
      pages.style.fontSize = "0.85em";
      card.appendChild(pages);
    }

    if (book.author) {
      const author = document.createElement("div");
      author.innerText = book.author;
      author.style.cssText = "font-size: 0.85em; color: gray;";
      card.appendChild(author);
    }

    if (showYear && book.year) {
      const badge = document.createElement("div");
      badge.innerText = book.year;
      badge.style.cssText = `
        background-color: ${getYearColor(book.year)};
        display: inline-block;
        padding: 2px 8px;
        margin-top: 4px;
        font-size: 0.75em;
        border-radius: 12px;
        color: #333;
      `;
      card.appendChild(badge);
    }

    container.appendChild(card);
  });

  container.style.maxHeight = container.scrollHeight + "px";
}

// Section toggle logic
let currentExpanded = null;

function toggleSection(section) {
  let books;
  let showYear = false;

  if (section === "read2025") books = booksRead2025;
  else if (section === "tbr") books = booksTBR;
  else if (section === "wishlist") books = booksWishlist;
  else if (section === "readAll") {
    books = booksReadAll;
    showYear = true;
  }

  if (currentExpanded === section) {
    container.style.maxHeight = "0";
    currentExpanded = null;
  } else {
    renderBooks(books, showYear);
    currentExpanded = section;
  }
}

This is it, if anyone could help it would be awesome. If anyoen has better code than this to do the same thing that’s cool too

It’s hard for me to test it now, but I think that for Obsidian, this book.file.path is just a string and not a link. I think you need to create an a element: Codeblock Reference - Dataview

dv.el("a", book.file.name, {
  href: book.file.path,
  style: "text-decoration: none; color: inherit;"
});

You could also use WikiLinks:

dv.paragraph("[[" + book.file.path + "|" + book.file.name + "]]");

Out of my head, not tested!!

Hope it can help you move further … cheers, Marko :nerd_face:

Using the WikiLinks worked!

There’s still an issue, because every time I click the button, in addition to showing my card gallery, it generates a list of all the notes in that gallery below it. And if I click again, the list doubles in size… I don’t know if this is going to be a problem when I get more notes…

Be that as it may, this is the closest and best I’ve been able to get from what I envisioned. Thank you so much!!!

1 Like

I’m guessing that some “array” needs to be empty before it’s generated (again). It’s a long function (nicely structured!) to check it out, so it’s hard for me to point out which line would be that :smiling_face:

In cases like that, I often use console.log() to see what and where something is generated.

Cheers, Marko :nerd_face: