CardHomeTab

approvedunreviewed

by guyu

A start page with user-defined cards. Card content lives in a single dashboard note, split by headings, with per-card CSS snippets.

↓ 298 downloadsUpdated 18d agoMIT

CardHomeTab

中文 · English

Turn one Markdown note into a start page. CardHomeTab splits that note by headings, renders every section as a card through Obsidian's own renderer, and lets each card carry its own CSS.

The plugin interface (commands, settings, notices) is currently available in Chinese only.

Features

  • A home view with a logo (built-in Lucide icon, vault image or URL), a wordmark, and a background image with separate light/dark sources, blur and dimming.
  • Fuzzy search over vault filenames, plus bookmarks and recent files. Enter opens the current tab, Ctrl/Cmd+Enter a new tab.
  • Card content comes from a single dashboard note, split by a configurable heading level.
  • Most content types render, because rendering goes through Obsidian's MarkdownRenderer: plain text, fenced code, base blocks, and dataview/dataviewjs blocks when Dataview is installed. The core query embedded-search block is the exception — inside a card it shows up as a plain code block with the query source rather than search results (see the limitations).
  • Cards live in a real note, so ![[table.base]], ![[image.png]] and relative links resolve against the dashboard note's folder.
  • Per-card metadata in a native %%card: %% comment: CSS snippets, column span, and icon.
  • Global per-content-type style options in the settings, starting with table zebra stripes.
  • Your own CSS snippets from the snippets folder, applied per card.
  • Reorder cards by dragging; edit card bodies in the ordinary note editor.
  • Cards render on mobile as well as desktop, using public APIs only — but the card action bar is a hover affordance, so on a touch device settings, edit, delete and drag are out of reach (see the limitations).
  • Cards follow along while you edit the dashboard note in a tab.

Installation

Community plugins

In Obsidian, open Settings → Community plugins → Browse and search for "CardHomeTab". This works once the plugin has been listed in the community directory.

Manual installation

  1. Download main.js, manifest.json and styles.css from the latest release — or run npm ci && npm run build and take them from dist/.
  2. Create <Vault>/.obsidian/plugins/card-home-tab/ in your vault.
  3. Copy the three files into that folder.
  4. Reload Obsidian, then enable "CardHomeTab" under Settings → Community plugins.

dist/ is a self-contained build output: every build cleans it and regenerates it, so its contents can be installed or published as-is.

The dashboard note

The default dashboard file is Home.md. A heading whose level equals the card heading level (2 by default) starts a card; the card body runs until the next heading of that level or a shallower one.

When the file does not exist: opening a new tab creates it with default content — two cards, one column each, each holding an inline base block in the cards view (one card per file, cardSize at the maximum of 800 so the cards render wide): recently modified (file.mtime, descending) and recently created (file.ctime, descending), 10 rows each, with the dashboard note itself excluded. Opening the home view by hand still stops at the "dashboard file not found" notice and waits for you to click "create and open" — on that path the usual cause is a mistyped path in the settings, and silently writing a file there would be worse.

The second card was meant to be "files you bookmarked", which is not possible: Bases has no bookmark property and no bookmark function, and bookmarks live in .obsidian/bookmarks.json, which Bases cannot read.

# My home

## Stats
%%card: css=stats; span=2; icon=lucide-chart%%
- Files in the vault: `1,204`

## To-do
%%card: css=user:todo%%
- [ ] Review the release notes

### This H3 stays inside the card
## Another card starts here
  • Headings shallower than the card heading level (the H1 title, for instance) are page-level: they never become a card, and one closes the card above it. A YAML frontmatter block is ignored as well.
  • Headings deeper than the card heading level render inside the card, as usual.
  • The heading text is the card title.
  • A section with no %%card: line is a plain card: span=1, no snippets, no icon.
  • The note is an ordinary note. It opens, edits, searches, syncs and diffs like any other.

The %%card: comment

The metadata line must be the first non-empty line after the heading.

%%card: css=mine; span=2; icon=lucide-chart%%
KeyValueMeaning
csscomma-separated snippet referencesCSS snippets applied to this card only
spaninteger ≥ 1how many grid columns the card spans
colinteger ≥ 1which column the card lives in; without it, columns are handed out round-robin in note order
iconan Obsidian icon id, e.g. lucide-charticon shown before the card title

Syntax rules:

  • Fields are separated by ;, multiple values by ,, key and value by =.
  • A snippet reference names a file in your snippets folder. A bare name and the user: prefix mean the same thing: css=mine is css=user:mine.
  • A reference that does not resolve is ignored, and the rest of the line still applies.
  • A col beyond the current column count is clamped to the last column; a col and span that together overflow fall back to a position that fits, so they never create an extra column.
  • Unrecognised keys are preserved as written.
  • %% %% is Obsidian's native comment syntax. It is hidden in reading mode and shown as dimmed text in Live Preview — that is Obsidian's behaviour, not extra markup added by the plugin.
  • The whole line is generated by the card settings dialog and can equally be written by hand.

Card layout

Cards are laid out by column: every card belongs to a column, cards stack top-down inside it and pull up tight, and columns do not affect one another. Dragging one card therefore never reshuffles the rest.

  • The column lives in the col key of %%card:. Cards without a col get columns round-robin in note order (1st → column 1, 2nd → column 2, …).
  • The first time you drag a card, the plugin writes the current column into every card that has no col yet. This happens once, it records what was already on screen, so the layout does not shift. Without it the round-robin fallback depends on the note position, and moving a card would shift every later card's position and make them jump columns.
  • Order inside a column follows note order, so dragging also reorders the sections in the note.
  • A card with span greater than 1 spans columns. It lands below the lowest free position of the columns it covers and pushes all of them down — a wide card couples the columns it spans at that point, which is the only sensible meaning of spanning. For drop targeting it counts as a member of every column it covers, so a card can be dropped above a wide card even when that wide card sits at the top.
  • The board fills the whole home view by default. Page → limit the board width caps it (the total width of every column plus the gaps between them) and centres it; the branding, the search bar and the background stay full width.
  • Below 900px of available width the grid collapses to a single column and col is ignored. That threshold looks at the width the pane offers, not at the capped board width, so setting a cap below 900 does not collapse the columns.

Content styles

These are set once in the settings and apply to every card, so the common cases need no CSS at all. They are independent of one another, combine freely, and every default equals what the plugin looked like before, so upgrading changes nothing. The rules ship in the plugin's styles.css and are driven by a class or a CSS variable on the home view, so going back to the defaults leaves no trace.

The settings page groups them into collapsible blocks per type. Every block — including Branding, Background and Search — has a reset button on the right of its header. It only shows up when that block differs from the defaults, and it restores just that block.

TypeSettingWhat it does
CardShow the card titleWith it off, the title and icon are hidden and the content starts at the very top. The drag handle and the settings, edit and delete buttons float to the top-right corner and appear on hover — they live in the same DOM layer as the title, so they are moved rather than hidden with it.
CardCorner radius0–24, defaults to 8 (the same as Obsidian's --radius-m).
CardCard gap0–48, defaults to 16. Applies to both directions.
CardBorder styleNone / solid / dashed / dotted, defaults to solid. Picking none is the same as setting the width to 0.
CardBorder width0–8, defaults to 1.
CardShadowNever / always / on hover, defaults to never. The strength comes from the theme's --shadow-s.
TableZebra stripesGives the data rows of a Markdown table alternating backgrounds. The header is untouched, hover still works, and tables rendered by base or Dataview are left alone.
BaseBar rowsPaints every row of a base table view as a full-width rounded block, with gaps instead of row and column separators. Cells stay side by side on one line: the Bases table is virtualised and JS positions its rows and cells, so this option only paints and never changes layout.
BaseHide the toolbarHides the control row at the top (view switcher, result count, sort, filter, properties), leaving only the content.

The colours can be overridden per theme or per vault: --home-tab-table-zebra for the stripes (which default to --background-modifier-hover), --home-tab-base-bar for the bar rows, and --home-tab-card-shadow for the card shadow.

More content types (base views, callouts) will follow the same pattern.

How per-card CSS snippets work

  • A reference is a bare file name or user:name — both read <config dir>/snippets/name.css, the same folder Obsidian's own appearance settings use.

  • Do not enable a card snippet in Settings → Appearance → CSS snippets: enabling it there applies it globally. The plugin reads the file itself and never touches enabledCssSnippets.

  • The snippet source is wrapped in

    @scope (.home-card.home-card[data-card-id="…"]) {
      /* your snippet, unchanged */
    }
    

    and attached to the document as a constructable stylesheet, so its rules can only match inside that one card. The scope class is repeated to lift specificity to 0,2,0, so theme selectors do not win over your snippet.

  • :root is rewritten to :scope. Inside @scope, :root still means the document root and would never match. Theme-prefixed selectors such as .theme-dark .foo keep working, because the theme class sits on body.

  • Snippets may not use @import. Such a snippet is rejected, a warning is logged, and the card renders without it; the other snippets on that card still apply.

  • Styles match the rendered DOM. Inside the scope:

    .home-card[data-card-id]
    ├── .home-card-header        icon, title, hover action bar
    └── .home-card-content.markdown-rendered
        └── .home-card-content-inner
            └── rendered Markdown
    

    markdown-rendered is added to .home-card-content itself, so .home-card-content.markdown-rendered p { … } is the form that matches.

Using the home view

Hover a card to reveal its action bar:

ActionBehaviour
Drag handleMoves the card to a position within a column. While dragging, a horizontal line hugs the card it will land under
Card settingsDialog for icon, CSS snippets (built-in and user, multi-select) and column span; writes back to the %%card:%% line and re-renders that card
EditOpens the dashboard note with the cursor on that card's heading
DeleteRemoves the section, after a confirmation

Cards are rendered read-only on the home view; all writing happens in the note.

Commands (the interface is in Chinese):

CommandMeaning
打开首页Open the home view
刷新所有卡片Re-render every card
在标签页打开仪表盘Open the dashboard note in a tab
新建卡片Append a new card section to the dashboard note

Settings

SectionSettings
PageDashboard file (a text field with note completion, so a path that does not exist yet can be typed in), card heading level (H2–H6), grid columns (1–6, defaults to 2), limit the board width and the board width itself (600–2400, only shown while the limit is on), replace new tabs, open home on startup
BrandingLogo type (none / built-in icon / vault image / URL), icon id or image path, logo scale (0.2–5), logo colour (empty follows the theme accent; built-in icons only), wordmark text, show wordmark, wordmark font size, wordmark font weight (100–900)
BackgroundBackground type (none / vault image / URL), light background, dark background (empty falls back to the light one), blur (0–40), dim (0–100)
SearchShow search bar, Markdown only, show path, show bookmarks, show recent files, max results (1–50), recent files count (0–20)
Content stylesCollapsible blocks per type: card (show title, radius, gap, border style and width, shadow), table (zebra stripes), base (bar rows, hide the toolbar)
SnippetsYour snippets with their paths, the snippet folder, and a "refresh list" button

Known limitations

  1. One dashboard file. There is a single dashboard note and no multi-dashboard switching.
  2. A card body cannot contain a heading of the same level as the card heading level. Such a heading starts a new card. Deeper headings are fine.
  3. Mixed line endings. updateCardMeta and appendCard always insert \n, so editing a CRLF dashboard note leaves the touched line with \n while the rest of the file keeps \r\n. Parsing is unaffected; the file just stops being uniformly CRLF on the line the plugin wrote.
  4. Settings are not indexed by Obsidian 1.13+'s settings search. The tab is imperative (display()), which minAppVersion 1.9.0 requires, and getSettingDefinitions() returns an empty array, so 1.13+ shows the page but its settings are not searchable.
  5. A %%card: value cannot contain ; (the field separator), and a token without = is not representable. Rather than silently dropping what it cannot represent, the plugin refuses to rewrite such a line and tells you to fix it by hand.
  6. CSS snippets must not use @import. Such a snippet is rejected and logged, and the card renders without it.
  7. No built-in snippets. The plugin ships no CSS snippets of its own. Recurring needs are covered by the content style options in the settings; anything else is your own snippet. A leftover css=auto is read as a snippet named auto, and a leftover css=builtin:x does not resolve at all — both are simply ignored.
  8. Obsidian 1.9.0 or newer is required. @scope needs Chromium 118+, and base blocks were introduced in 1.9.0, so minAppVersion is 1.9.0. All features use public APIs, which is why the plugin loads on mobile and the cards render there.
  9. The last column stays empty when span does not divide the column count. With three columns and every card at span=2, the cards can only occupy columns 1-2 one after another and nothing ever reaches column 3. That is inherent to spanning (a second span-2 card does not fit in three columns), not a layout bug; such a dashboard wants a column count of 2.
  10. Content styles cover Markdown content only. Table zebra stripes deliberately skip the tables that base views and Dataview render, because those bring their own row markup and theme styling.
  11. Non-goals. Cards cannot be folded, nested or edited inline, and a card cannot live in its own file — a card is a section of the dashboard note and nothing more. The %%card: %% line is visible as dimmed text in Live Preview.
  12. The card action bar is hover-only. It is revealed by :hover/:focus-within on the card, and its controls are non-focusable spans. A touch device has no hover and there is no keyboard route to them, so settings, edit, delete and drag are desktop-only. The cards themselves render on mobile.
  13. The dashboard file must be a Markdown note. A path that does not end in .md is refused: the home view reports the file as missing, and creating it fails with a notice. It cannot be used to point the plugin at, for instance, a .canvas.
  14. Two commands create the dashboard file when it is missing. 新建卡片 and 在标签页打开仪表盘 create the note at whatever path the setting currently holds, without asking; 新建卡片 then appends a card to it. A typo in the path leaves a stray note behind instead of failing.
  15. The core query embedded-search block produces no results. Obsidian only turns a query block into an embedded search under a reading-mode MarkdownView host. The plugin renders through the static MarkdownRenderer.render, which has no such host, so a query block falls back to a plain code block showing the query source instead of search results. dataview and base blocks are unaffected (they are properly registered code-block processors). Use a dataview or base block for a dynamic list.

License

MIT

For plugin developers

Search results and similarity scores are powered by semantic analysis of your plugin's README. If your plugin isn't appearing for searches you'd expect, try updating your README to clearly describe your plugin's purpose, features, and use cases.