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.
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.
Enteropens the current tab,Ctrl/Cmd+Entera 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,baseblocks, anddataview/dataviewjsblocks when Dataview is installed. The corequeryembedded-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
- Download
main.js,manifest.jsonandstyles.cssfrom the latest release — or runnpm ci && npm run buildand take them fromdist/. - Create
<Vault>/.obsidian/plugins/card-home-tab/in your vault. - Copy the three files into that folder.
- 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%%
| Key | Value | Meaning |
|---|---|---|
css | comma-separated snippet references | CSS snippets applied to this card only |
span | integer ≥ 1 | how many grid columns the card spans |
col | integer ≥ 1 | which column the card lives in; without it, columns are handed out round-robin in note order |
icon | an Obsidian icon id, e.g. lucide-chart | icon 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=mineiscss=user:mine. - A reference that does not resolve is ignored, and the rest of the line still applies.
- A
colbeyond the current column count is clamped to the last column; acolandspanthat 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
colkey of%%card:. Cards without acolget 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
colyet. 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
spangreater 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
colis 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.
| Type | Setting | What it does |
|---|---|---|
| Card | Show the card title | With 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. |
| Card | Corner radius | 0–24, defaults to 8 (the same as Obsidian's --radius-m). |
| Card | Card gap | 0–48, defaults to 16. Applies to both directions. |
| Card | Border style | None / solid / dashed / dotted, defaults to solid. Picking none is the same as setting the width to 0. |
| Card | Border width | 0–8, defaults to 1. |
| Card | Shadow | Never / always / on hover, defaults to never. The strength comes from the theme's --shadow-s. |
| Table | Zebra stripes | Gives 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. |
| Base | Bar rows | Paints 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. |
| Base | Hide the toolbar | Hides 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. -
:rootis rewritten to:scope. Inside@scope,:rootstill means the document root and would never match. Theme-prefixed selectors such as.theme-dark .fookeep working, because the theme class sits onbody. -
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 Markdownmarkdown-renderedis added to.home-card-contentitself, so.home-card-content.markdown-rendered p { … }is the form that matches.
Using the home view
Hover a card to reveal its action bar:
| Action | Behaviour |
|---|---|
| Drag handle | Moves the card to a position within a column. While dragging, a horizontal line hugs the card it will land under |
| Card settings | Dialog for icon, CSS snippets (built-in and user, multi-select) and column span; writes back to the %%card:%% line and re-renders that card |
| Edit | Opens the dashboard note with the cursor on that card's heading |
| Delete | Removes 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):
| Command | Meaning |
|---|---|
打开首页 | 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
| Section | Settings |
|---|---|
| Page | Dashboard 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 |
| Branding | Logo 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) |
| Background | Background type (none / vault image / URL), light background, dark background (empty falls back to the light one), blur (0–40), dim (0–100) |
| Search | Show search bar, Markdown only, show path, show bookmarks, show recent files, max results (1–50), recent files count (0–20) |
| Content styles | Collapsible blocks per type: card (show title, radius, gap, border style and width, shadow), table (zebra stripes), base (bar rows, hide the toolbar) |
| Snippets | Your snippets with their paths, the snippet folder, and a "refresh list" button |
Known limitations
- One dashboard file. There is a single dashboard note and no multi-dashboard switching.
- 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.
- Mixed line endings.
updateCardMetaandappendCardalways insert\n, so editing a CRLF dashboard note leaves the touched line with\nwhile the rest of the file keeps\r\n. Parsing is unaffected; the file just stops being uniformly CRLF on the line the plugin wrote. - Settings are not indexed by Obsidian 1.13+'s settings search. The tab is imperative (
display()), whichminAppVersion1.9.0 requires, andgetSettingDefinitions()returns an empty array, so 1.13+ shows the page but its settings are not searchable. - 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. - CSS snippets must not use
@import. Such a snippet is rejected and logged, and the card renders without it. - 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=autois read as a snippet namedauto, and a leftovercss=builtin:xdoes not resolve at all — both are simply ignored. - Obsidian 1.9.0 or newer is required.
@scopeneeds Chromium 118+, andbaseblocks were introduced in 1.9.0, sominAppVersionis 1.9.0. All features use public APIs, which is why the plugin loads on mobile and the cards render there. - The last column stays empty when
spandoes not divide the column count. With three columns and every card atspan=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. - Content styles cover Markdown content only. Table zebra stripes deliberately skip the tables that
baseviews and Dataview render, because those bring their own row markup and theme styling. - 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. - The card action bar is hover-only. It is revealed by
:hover/:focus-withinon the card, and its controls are non-focusablespans. 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. - The dashboard file must be a Markdown note. A path that does not end in
.mdis 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. - 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. - The core
queryembedded-search block produces no results. Obsidian only turns aqueryblock into an embedded search under a reading-modeMarkdownViewhost. The plugin renders through the staticMarkdownRenderer.render, which has no such host, so aqueryblock falls back to a plain code block showing the query source instead of search results.dataviewandbaseblocks are unaffected (they are properly registered code-block processors). Use adatavieworbaseblock for a dynamic list.
License
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.