Vertical Timeline Dataview: pull dates from notes and visualize them in a vertical timeline note

Disclaimer

Is this project open source? Yes
Is this project completely free? Yes
Is this project vibe-coded beyond the author’s ability to comprehend how it works? Yes
Community Directory: N/A (This is a workflow showcase / CSS snippet, not a plugin)


Vertical Timeline View (DataviewJS + CSS snippet)

Hey there,
first up, I am not a programmer, but my drive for perfectionism never let’s me rest, so with the help of heavy vibe coding I created this and thought it still might be worth to share. I made sure that the code is clean and should work regarding your theme, is highly customizable and shouldn’t break with the next update. If there is any issue, I will try to fix it to the best of my capabilities.
Support for the Style Settings plugin was added, but maybe will be expanded on in the future.

What’s it for?

This is a custom DataviewJS code that searches your notes for specific frontmatter dates and organizes them into a clean, interactive and adaptive, vertical timeline layout using custom CSS. It supports customization options and should work regardless of the theme as well as light and dark modes.

Step 0: Plugins

To use this, you need to install the following community plugins:

  • Dataview (Ensure Enable JavaScript Queries is turned on in the plugin settings)
  • Optional: StyleSettings (Allows changing a few variables to customize the look)

Step 1: Create the CSS Snippet

  1. Open your vault settings and navigate to Appearance.
  2. Under the CSS snippets section, click the folder icon to open the snippets directory.
  3. Right-Click, to create a new text file named timeline.css.
  4. Paste the following code into it:

Code

/* @settings
name: Vertical Timeline View
id: vertical-timeline-view-settings
settings:
  - id: timeline-max-width
    title: Timeline Max Width
    type: variable-number
    default: 750
    format: px
  - id: timeline-card-radius
    title: Card Border Radius
    type: variable-number
    default: 10
    format: px
  - id: timeline-row-gap
    title: Space Between Cards
    type: variable-number
    default: 0.85
    format: rem
  - id: timeline-title-size
    title: Title Text Size
    type: variable-number
    default: 1.25
    format: rem
  - id: timeline-body-size
    title: Body Text Size
    type: variable-number
    default: 0.95
    format: rem
  - id: hide-timeline-properties
    title: Hide Page Properties
    description: When enabled, file properties (YAML metadata) will be completely hidden.
    type: class-toggle
*/

/* Initialize variables */
:root {
    --timeline-max-width: 750px;
    --timeline-card-radius: 10px;
    --timeline-row-gap: 0.85rem;
    --timeline-title-size: 1.25rem;
    --timeline-body-size: 0.95rem;
}

/* =========================================================================
   1. TABLE WRAPPER & CONTAINER ISOLATION
   ========================================================================= */

.custom-timeline-view table.dataview.table-view-table {
    display: block !important;
    width: 100% !important;
    max-width: var(--timeline-max-width) !important;
    margin: 1rem auto !important;
    border-collapse: collapse !important;
    border: none !important;
    background: transparent !important;
}

.custom-timeline-view thead,
.custom-timeline-view th {
    display: none !important;
}

.custom-timeline-view tbody {
    display: flex !important;
    flex-direction: column !important;
    gap: var(--timeline-row-gap) !important; 
    width: 100% !important;
    border: none !important;
    background: transparent !important;
}

/* =========================================================================
   2. CARD LAYOUT AND INTERACTION ENGINE
   ========================================================================= */

.custom-timeline-view tbody tr {
    display: flex !important;
    flex-direction: column !important;
    width: 100% !important;
    padding: 1.2rem !important;
    box-sizing: border-box !important;
    position: relative !important; 
    background-color: var(--background-primary) !important;
    border-radius: var(--timeline-card-radius) !important;
    border: 1px solid var(--background-modifier-border) !important;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06) !important;
    margin: 0 !important;
    transition: transform 0.2s ease, box-shadow 0.2s ease !important;
}

.custom-timeline-view tbody tr:not(.timeline-birthday-row):hover,
.custom-timeline-view .timeline-group-block tr:not(.timeline-birthday-row):hover,
.custom-timeline-view table.dataview.table-view-table tbody tr:not(.timeline-birthday-row):hover {
    background-color: var(--background-primary) !important;
    --table-row-hover-background: var(--background-primary) !important;
    transform: translateY(-1px) !important;
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12) !important;
    z-index: 10 !important;
}

.custom-timeline-view tbody tr[data-custom-bg]:not(.timeline-birthday-row):hover {
    transform: translateY(-1px) !important;
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.2) !important;
    z-index: 10 !important;
}

/* =========================================================================
   3. INTERNAL CELL STRUCTURE & TYPOGRAPHY
   ========================================================================= */

.custom-timeline-view tbody td {
    display: block !important;
    width: 100% !important;
    max-width: 100% !important;
    border: none !important;
    padding: 0.2rem 0 !important;
    background: transparent !important;
}

.custom-timeline-view tbody tr td:nth-child(1) {
    font-weight: bold !important;
    color: var(--text-accent) !important;
    font-size: 0.9rem !important;
    text-transform: uppercase !important;
    letter-spacing: 0.05em !important;
    box-sizing: border-box !important;
    min-width: max-content !important; 
    overflow: visible !important;       
    white-space: nowrap !important;     
}

.custom-timeline-view tbody tr td:nth-child(3) {
    display: flex !important;
    align-items: center !important;
    flex-wrap: wrap !important;
    font-size: var(--timeline-title-size) !important;
    margin-bottom: 0.5rem !important;
}

.custom-timeline-view tbody tr:not(.timeline-birthday-row) td:nth-child(3) a.internal-link {
    color: var(--text-normal) !important; 
    font-weight: bold !important; 
    text-decoration: none !important;
    border-bottom: none !important;
    box-shadow: none !important;
    pointer-events: none !important; 
}

.custom-timeline-view tbody tr td:nth-child(4) {
    font-size: var(--timeline-body-size) !important;
    line-height: 1.5 !important;
    padding-top: 0.6rem !important;
    margin-top: 0.4rem !important;
    position: relative !important;
    z-index: 6 !important; 
    border-top: 1px dashed var(--background-modifier-border) !important; 
}

.custom-timeline-view tbody tr:not([data-custom-bg]):not([data-force-dark-text]) td:nth-child(4) {
    color: var(--text-normal) !important; 
}

.custom-timeline-view .timeline-timestamp {
    font-style: italic !important;
    font-weight: normal !important;
    font-size: calc(var(--timeline-title-size) * 0.85) !important;
    margin-right: 0.5rem !important; 
    display: inline-block !important;
    color: var(--text-muted) !important; 
}

.custom-timeline-view tbody tr:not(.timeline-birthday-row) td:nth-child(3) a.internal-link .iconize-icon,
.custom-timeline-view tbody tr:not(.timeline-birthday-row) td:nth-child(3) a.internal-link svg,
.custom-timeline-view tbody tr:not(.timeline-birthday-row) td:nth-child(3) a.internal-link img {
    display: none !important;
}

/* =========================================================================
   4. NATIVE CLICKABLE CONTAINER OVERLAY SYSTEM
   ========================================================================= */

.custom-timeline-view tbody tr:not(.timeline-birthday-row) td:nth-child(2) {
    display: block !important;
    position: static !important;
    height: 0px !important;
    padding: 0 !important;
    margin: 0 !important;
}

.custom-timeline-view tbody tr:not(.timeline-birthday-row) td:nth-child(2) a.internal-link {
    position: absolute !important;
    top: 0 !important;
    left: 0 !important;
    right: 0 !important;
    bottom: 0 !important;
    width: 100% !important;
    height: 100% !important;
    font-size: 0 !important; 
    color: transparent !important;
    background: transparent !important;
    border: none !important;
    box-shadow: none !important;
    z-index: 5 !important; 
}

/* =========================================================================
   5. COLOR ROLE DESIGNATION OVERRIDES
   ========================================================================= */

.custom-timeline-view tbody tr[data-custom-bg] td:nth-child(3) a.internal-link { color: #ffffff !important; }
.custom-timeline-view tbody tr[data-custom-bg] .timeline-timestamp { color: rgba(255, 255, 255, 0.6) !important; }
.custom-timeline-view tbody tr[data-custom-bg] td:nth-child(4) { color: rgba(255, 255, 255, 0.9) !important; }
.custom-timeline-view tbody tr[data-custom-bg] td:nth-child(4) { border-top-color: rgba(255, 255, 255, 0.25) !important; }

.custom-timeline-view tbody tr[data-force-dark-text] td:nth-child(3) a.internal-link { color: #333333 !important; }
.custom-timeline-view tbody tr[data-force-dark-text] .timeline-timestamp { color: #555555 !important; }
.custom-timeline-view tbody tr[data-force-dark-text] td:nth-child(4) { color: #222222 !important; }
.custom-timeline-view tbody tr[data-force-dark-text] td:nth-child(4) { border-top-color: rgba(0, 0, 0, 0.15) !important; }

/* =========================================================================
   6. COMPLEX MULTI-LOG GROUPING BLOCKS
   ========================================================================= */

.custom-timeline-view .timeline-group-block {
    display: flex !important;
    flex-direction: column !important;
    width: 100% !important;
    border-radius: var(--timeline-card-radius) !important;
    overflow: visible !important; 
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08) !important;
    border: 1px solid var(--background-modifier-border) !important;
}

.custom-timeline-view .timeline-group-block tr {
    border: none !important;
    box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06) !important;
    border-radius: 0px !important;
}

.custom-timeline-view .timeline-group-block tr + tr {
    border-top: 1px solid var(--background-modifier-border) !important;
}

.custom-timeline-view .timeline-group-block tr:first-child {
    border-top-left-radius: var(--timeline-card-radius) !important;
    border-top-right-radius: var(--timeline-card-radius) !important;
}
.custom-timeline-view .timeline-group-block tr:last-child {
    border-bottom-left-radius: var(--timeline-card-radius) !important;
    border-bottom-right-radius: var(--timeline-card-radius) !important;
}

.custom-timeline-view .timeline-group-block tr.same-day-card td:nth-child(1) {
    display: none !important;
}

/* =========================================================================
   7. MINIMALIST BIRTHDAY ROW STRUCTURAL COMPONENT
   ========================================================================= */

.custom-timeline-view tbody tr.timeline-birthday-row {
    background-color: transparent !important;
    border: none !important;
    box-shadow: none !important;
    padding: 0 !important;
    margin-top: 0 !important;
    margin-bottom: 0 !important;
    gap: 0 !important;
}

.custom-timeline-view tbody tr.timeline-birthday-row + tr.timeline-birthday-row {
    margin-top: 0.85rem !important; 
}

.custom-timeline-view tbody tr.timeline-birthday-row td:nth-child(1) {
    display: block !important;
    width: 100% !important;
    max-width: 100% !important;
    padding-left: 1.2rem !important; 
    padding-bottom: 0px !important;
    margin-bottom: 0px !important;
    color: var(--text-normal) !important;
}

.custom-timeline-view tbody tr.timeline-birthday-row td:nth-child(2),
.custom-timeline-view tbody tr.timeline-birthday-row td:nth-child(4) {
    display: none !important;
}

.custom-timeline-view tbody tr.timeline-birthday-row td:nth-child(3) {
    display: flex !important;
    align-items: center !important;
    width: 100% !important;
    padding-left: 1.2rem !important; 
    margin-top: 0.5rem !important;    
    margin-bottom: 0px !important;
}

.custom-timeline-view tbody tr.timeline-birthday-row td:nth-child(3) a.internal-link {
    display: inline-flex !important;
    align-items: center !important;
    pointer-events: auto !important; 
    font-weight: bold !important; 
    text-decoration: none !important;
    border-bottom: none !important;
    box-shadow: none !important;
    color: var(--text-normal) !important;
}

.custom-timeline-view tbody tr.timeline-birthday-row:hover {
    background-color: transparent !important;
    transform: none !important;
    box-shadow: none !important;
    z-index: initial !important;
}

/* =========================================================================
   8. OPTIONAL PROPERTY INTERFACES (STYLE SETTINGS CONTROLLED)
   ========================================================================= */

body.hide-timeline-properties .metadata-container {
    display: none !important;
}
  1. Save the file (Strg + S) and return to the CSS snippets section in Obsidian.
  2. Refresh and toggle the timeline snippet on.

Step 2: Create the Timeline Note

  1. Create a new note, where you want your timeline to appear, anywhere in your vault.
  2. Paste the following Frontmatter into it:

Code

---
sort: asc
custom-presets:
  - "vintage-pastel-green: #D3DFB8, dark"
---

Notes

  • sort: (optional) Choose between asc and desc for an ascending or descending sort. If left empty the timeline is sorted in ascending order beginning with the oldest date at the top.
  • custom-presets: (optional) Add custom card color presets by defining a reference name, hex code and light or dark default text. Use the code above as reference for each entry.
  1. Paste the following DataviewJS code below the Frontmatter into it:

Code

```dataviewjs
// Initialization: Bind custom class targeting for modular timeline rendering overrides
this.container.addClass("custom-timeline-view");

// Fetch the sort preference from frontmatter (Default to "asc" if not defined)
const sortOrder = dv.current().sort === "desc" ? "desc" : "asc";

// Multi-language chronological index routing for explicit textual strings
const monthMap = {
    januar: 1, feb: 2, februar: 2, märz: 3, maerz: 3, april: 4,
    mai: 5, juni: 6, juli: 7, august: 8, september: 9, 
    oktober: 10, november: 11, dezember: 12,
    jan: 1, january: 1, february: 2, mar: 3, march: 3, apr: 4, 
    april: 4, may: 5, jun: 6, june: 6, jul: 7, july: 7, aug: 8, 
    august: 8, sep: 9, september: 9, oct: 10, october: 10, 
    nov: 11, november: 11, dec: 12, december: 12
};

// Normalization Engine: Convert custom string timestamps into primitive Unix integers
const getAbsoluteTimestamp = (dateVal) => {
    if (!dateVal) return 0;
    const str = String(dateVal).trim().toLowerCase();
    
    if (/^\d{4}-\d{2}-\d{2}$/.test(str)) {
        return new Date(str).getTime();
    }
    
    const match = str.match(/^(\d+)\.\s*([a-zä]+)\s*(\d{4})$/);
    if (match) {
        const day = parseInt(match[1], 10);
        const monthName = match[2];
        const year = parseInt(match[3], 10);
        const monthNum = monthMap[monthName] || 1;
        return new Date(year, monthNum - 1, day).getTime();
    }
    
    return Date.parse(str) || 0;
};

// Normalization Engine: Clean textual log timestamps down to structural standard strings
const getNormalizedTime = (timeVal) => {
    if (!timeVal) return "00:00"; 
    let cleanTime = String(timeVal).replace(/uhr/i, "").trim();
    
    if (cleanTime.includes(":")) {
        let [hours, minutes] = cleanTime.split(":");
        hours = hours.padStart(2, "0");
        minutes = minutes.padEnd(2, "0");
        return `${hours}:${minutes}`;
    }
    return cleanTime;
};

// Data Pipeline: Fetch and ingest page tags
const allPages = dv.pages("#timeline-event or #character");
const flattenedEvents = [];

allPages.forEach(p => {
    const isCharacter = p.file.tags.includes("#character");
    const dateField = p["timeline-date"];
    
    if (!dateField) return;

    const dateArray = Array.isArray(dateField) ? dateField : [dateField];

    dateArray.forEach((singleDate, arrayIdx) => {
        const currentDateStr = singleDate ? String(singleDate).trim() : "—";

        if (isCharacter) {
            const timeField = p["timeline-time"];
            const singleTime = Array.isArray(timeField) ? timeField[arrayIdx] : timeField;

            flattenedEvents.push({
                date: currentDateStr,
                time: singleTime || "00:01",
                link: p.file.link,
                isBirthday: true,
                rawPage: p
            });
        } else {
            flattenedEvents.push({
                date: currentDateStr,
                time: p["timeline-time"],
                link: p.file.link,
                isBirthday: false,
                rawPage: p
            });
        }
    });
});

// Data Pipeline: Sort dynamically by Frontmatter Direction
flattenedEvents.sort((a, b) => {
    const timeA = getAbsoluteTimestamp(a.date);
    const timeB = getAbsoluteTimestamp(b.date);
    
    if (timeA !== timeB) {
        return sortOrder === "desc" ? timeB - timeA : timeA - timeB;
    }
    
    const clockA = getNormalizedTime(a.time);
    const clockB = getNormalizedTime(b.time);
    return sortOrder === "desc" ? clockB.localeCompare(clockA) : clockA.localeCompare(clockB);
});

// Layout Generation: Construct structural matrix data and build table layout
if (flattenedEvents.length > 0) {
    let lastDate = "";
    const sameDayIndices = [];
    
    const rows = flattenedEvents.map((ev, index) => {
        if (index > 0 && ev.date === lastDate && ev.date !== "—") {
            sameDayIndices.push(index);
        } else {
            lastDate = ev.date;
        }
        
        if (ev.isBirthday) {
            return [ ev.date, "", `🎂 `, ev.link ];
        } else {
            const timeStr = ev.time ? `<span class="timeline-timestamp">${ev.time}</span>` : "";
            const displayTitle = ev.rawPage["timeline-title"] || ev.rawPage.file.name;
            const targetPath = ev.link.path.replace(/"/g, '&quot;');
            const HTMLTitleBlock = `${timeStr}<a class="internal-link timeline-title-link" href="${targetPath}">${displayTitle}</a>`;
            
            return [
                ev.date,       
                ev.link,                     
                HTMLTitleBlock,     
                ev.rawPage["timeline-desc"] || "—"        
            ];
        }
    });

    dv.table(["Date", "Event Note", "Display Title", "Description Log"], rows);

    const tableComponent = this.container.querySelector("table.table-view-table");
    if (tableComponent) {
        const trElements = Array.from(tableComponent.querySelectorAll("tbody tr"));

        flattenedEvents.forEach((ev, idx) => {
            const row = trElements[idx];
            if (!row) return;

            // DOM Transformation: Restructure row elements for Birthday layouts
            if (ev.isBirthday) {
                row.classList.add("timeline-birthday-row"); 
                row.classList.remove("same-day-card"); 
                
                row.querySelectorAll("td").forEach(td => {
                    td.style.setProperty("background", "transparent", "important");
                });

                const dateCell = row.querySelector("td:nth-child(1)");
                const ghostCell = row.querySelector("td:nth-child(2)");
                const iconCell = row.querySelector("td:nth-child(3)");
                const nameCell = row.querySelector("td:nth-child(4)");

                if (dateCell) dateCell.style.setProperty("color", "var(--text-muted)", "important");
                if (ghostCell) ghostCell.style.setProperty("display", "none", "important");

                if (iconCell && nameCell) {
                    iconCell.style.setProperty("font-weight", "500", "important");
                    iconCell.style.setProperty("font-style", "normal", "important");
                    iconCell.innerHTML = `<span>🎂&nbsp; </span>`;
                    
                    const linkNode = nameCell.firstChild || nameCell;
                    if (linkNode && linkNode.style) {
                        linkNode.style.setProperty("font-style", "normal", "important");
                    }
                    
                    iconCell.appendChild(linkNode);
                    nameCell.style.setProperty("display", "none", "important");
                }
                return; 
            }

            // Color Engine: Map styling keywords to hexadecimal palettes
            const presets = {
                // Obsidian Callout Presets
                note:      { color: "#E5ECF8", defaultText: "dark" },
                info:      { color: "#DEF1F4", defaultText: "dark" },
                todo:      { color: "#DEF1F4", defaultText: "dark" },
                tip:       { color: "#DEF1EF", defaultText: "dark" },
                hint:      { color: "#DEF1EF", defaultText: "dark" },
                important: { color: "#DEF1EF", defaultText: "dark" },
                success:   { color: "#DEF2E6", defaultText: "dark" },
                check:     { color: "#DEF2E6", defaultText: "dark" },
                done:      { color: "#DEF2E6", defaultText: "dark" },
                question:  { color: "#E8F5E0", defaultText: "dark" },
                help:      { color: "#E8F5E0", defaultText: "dark" },
                faq:       { color: "#E8F5E0", defaultText: "dark" },
                warning:   { color: "#F8EDDE", defaultText: "dark" },
                caution:   { color: "#F8EDDE", defaultText: "dark" },
                attention: { color: "#F8EDDE", defaultText: "dark" },
                failure:   { color: "#F8E6E6", defaultText: "dark" },
                fail:      { color: "#F8E6E6", defaultText: "dark" },
                missing:   { color: "#F8E6E6", defaultText: "dark" },
                danger:    { color: "#F8E0E5", defaultText: "dark" },
                error:     { color: "#F8E0E5", defaultText: "dark" },
                bug:       { color: "#F7DEE7", defaultText: "dark" },
                example:   { color: "#EBE6F8", defaultText: "dark" },
                quote:     { color: "#EEEEEE", defaultText: "dark" },
                cite:      { color: "#EEEEEE", defaultText: "dark" }
            };

            // Dynamic Frontmatter Preset Parser
            const rawCustomPresets = dv.current()["custom-presets"];
            if (rawCustomPresets) {
                const presetArray = Array.isArray(rawCustomPresets) ? rawCustomPresets : [rawCustomPresets];
                presetArray.forEach(str => {
                    if (!str || !str.includes(":")) return;
                    
                    const mainParts = str.split(":");
                    const name = mainParts[0].trim().toLowerCase();
                    const valueData = mainParts.slice(1).join(":").trim();
                    
                    let colorValue = valueData;
                    let textValue = "dark";
                    
                    if (valueData.includes(",")) {
                        const dataParts = valueData.split(",");
                        colorValue = dataParts[0].trim();
                        textValue = dataParts[1].trim().toLowerCase();
                    }
                    
                    colorValue = colorValue.replace(/['"},\s]/g, "");
                    textValue = textValue.replace(/['"},\s]/g, "");
                    
                    if (name && colorValue) {
                        presets[name] = { color: colorValue, defaultText: textValue };
                    }
                });
            }

            const p = ev.rawPage;
            let customColor = p["timeline-color"];
            let presetDefaultText = "";

            if (customColor) {
                const lookup = String(customColor).trim().toLowerCase();
                if (presets[lookup]) {
                    customColor = presets[lookup].color;
                    presetDefaultText = presets[lookup].defaultText;
                }
            }

            const manualTextSetting = p["timeline-text"] ? String(p["timeline-text"]).trim().toLowerCase() : "";
            
            let finalDarkText = false;
            if (manualTextSetting === "dark" || manualTextSetting === "true" || presetDefaultText === "dark") {
                finalDarkText = true;
            }

            // DOM Transformation: Assign color flags for CSS dark/light mode balancing
            if (customColor) {
                row.style.setProperty("background-color", customColor, "important");
                if (finalDarkText) {
                    row.setAttribute("data-force-dark-text", "true");
                    row.removeAttribute("data-custom-bg");
                } else {
                    row.setAttribute("data-custom-bg", customColor);
                    row.removeAttribute("data-force-dark-text");
                }
            } else {
                row.removeAttribute("data-custom-bg");
                row.removeAttribute("data-force-dark-text");
            }

            if (sameDayIndices.includes(idx) && !ev.isBirthday) {
                row.classList.add("same-day-card");
            }
        });

        // DOM Transformation: Wrap same-day sequential cards into grouping containers
        let currentGroup = null;
        trElements.forEach((row, idx) => {
            const ev = flattenedEvents[idx];
            const isFollower = sameDayIndices.includes(idx) && !ev.isBirthday;
            const hasFollower = sameDayIndices.includes(idx + 1) && !flattenedEvents[idx + 1]?.isBirthday;

            if (!isFollower && hasFollower) {
                currentGroup = document.createElement("div");
                currentGroup.className = "timeline-group-block";
                row.parentNode.insertBefore(currentGroup, row);
                currentGroup.appendChild(row);
                row.classList.add("group-lead");
            } else if (isFollower && currentGroup) {
                currentGroup.appendChild(row);
                if (!hasFollower) {
                    row.classList.add("group-tail");
                    currentGroup = null;
                }
            }
        });
    }
} else {
    dv.paragraph("> [!minder] No events found.");
}
```

Grouping & Stacking

The code above automatically groups and stacks entries with the same date, removing redundant date lines and unnecessary spaces between the entries.

Step 3: Create a Timeline Event Note

These are notes tagged with #timeline-event that show up as regular cards in your Timeline Note. The content of the note is fully customizable, but the Frontmatter needs to at least contain the following code.

Code

---
tags:
  - timeline-event
timeline-date: 2026-06-24
timeline-time: "14:30 o'clock"
timeline-title: "The Great Fire"
timeline-desc: "A massive fire broke out near the harbor, destroying three main warehouses."
timeline-color: #333333
timeline-text: light
---

Notes

  • tags: (mandatory) Each Timeline Event Note needs to at least contain #timeline-event.
  • timeline-date: (mandatory) Supports the YYYY-MM-DD and DD. MMMM YYYY date formats and automatically sorts English and German month names.
  • timeline-time: (optional) Supports the 24h time format with or without additional text behind and sorts accordingly.
  • timeline-title: (optional) Add a custom card title. If left empty it defaults to the note name.
  • timeline-desc: (optional) Add a custom card description. If left empty it adds instead.
  • timeline-color: Used to reference custom-presets defined previously in the Timeline Note Frontmatter, custom hex codes such as #333333, predefined presets for each Obsidian callout name listed below.
Property Value Visual Style Context
note / quote / cite Soft Gray
info / todo Soft Blue
tip / hint / important Soft Teal
success / check / done Soft Green
question / help / faq Soft Lime Green
warning / caution / attention Soft Amber/Orange
failure / fail / missing Soft Crimson
danger / error Soft Rose
bug Soft Pink
example Soft Purple
  • timeline-text: (optional) Used to override the text color when using a custom hex code for the timeline-color. Valid values are light/false or dark/true.

Step 4: (optional) Create a Character Note

These are notes tagged with #character that have a different and more simple styling and allow to add character’s birthdays into the sorting of the timeline. The content of the note is fully customizable, but the Frontmatter needs to at least contain the following code.
Note: The character entries in the Timeline Note will automatically render as a streamlined row prefixed with a :birthday_cake: emoji and followed by the name of the note.

Code

---
tags:
  - character
timeline-date: 
  - 2026-05-12
timeline-time: "08:15"          # Optional: Birth time (Defaults to "00:01" for sorting preservation)
---

Notes

  • tags: (mandatory) Each Character Note needs to at least contain #character.
  • timeline-date: (mandatory) List each birthday that you wish to display in the Timeline Note. Supports the YYYY-MM-DD and DD. MMMM YYYY date formats and automatically sorts English and German month names.
  • timeline-time: (optional) Add a specific time of birth for sorting. If left empty it defaults to 00:01.

(optional) Customization Options

If you are using the Style Settings plugin, you can adjust the following variables:

  • Timeline Max Width: (default: 750) Changes the width of all cards.
  • Card Border Radius: (default: 10) Changes the border radius of all cards, but remains the stacked look.
  • Space Between Cards: (default: 0.85) Changes the spacing between cards.
  • Title Text Size: (default: 1.25) Changes the text size of the date and title.
  • Body Text Size: (default: 0.95) Changes the text size of the description.
  • Hide Page Properties: Hides the properties of the Timeline Note (YAML metadata) when enabled.

To-Do

  • Add Screenshots to this guide
  • Add more features to the compatibility with the Style Settings plugin

This dataview timeline configuration is actually pretty good for Obsidian. +1 date + time normalisation works nicely, sorting mechanism is tidy.

sole weakness is the DOM grouping aspect since dataview re-renders may occasionally disrupt manual row wrapping, particularly on larger vaults.

generally however, this is considerably more complex than the standard snippet things for a dataview based vertical timeline.

1 Like

Can you provide a screenshot? I’m having a hard time understanding what the goal is here.

1 Like

I am currently working on some more screenshots that are going to be implemented over time, but for now this is how it is supposed to look:


If you have any questions, please let me know and thank you for checking it out!

1 Like

Thank you for your comment. Currently I am not able to edit my original post, since I am a new user of this forum, but I already noticed that the date format YYYY-MM-DD doesn’t work properly. Please excuse this and I will try to upload a fix later on. If you have any other suggestions, please comment below.