* enhance(plugins): add block properties and block renderers with conditional rendering * chore(libs): bump version * enhance(docs): update Experiments API guide with detailed usage examples and best practices * enhance(ui): implement plugin renderer effects and enhance action bar visibility * enhance(plugin): improve router refresh mechanism for dynamic plugin routes * chore(libs): rename development notes and add SKILL.md for plugin SDK * enhance(plugin): enhance PluginLogger with structured log entries and UI for viewing logs * fix: lint * Update src/main/frontend/components/block.cljs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/main/frontend/handler/plugin.cljs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/main/frontend/handler/plugin.cljs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update libs/src/modules/LSPlugin.Experiments.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/main/frontend/handler/plugin.cljs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * enhance(plugin): add rich text interpolation to i18n translation * fix: lint * fix: lint * fix: remove unnecessary newlines and clean up code formatting * fix: lint * fix(libs): incorrect entry path for js mode * enhance(docs): add custom theme plugin guide and update SKILL.md * enhance(plugin): add 'open logs' option in multiple languages --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
25 KiB
Custom Theme Plugin Guide
This guide summarizes Logseq's current UI theme architecture and gives a practical workflow for building a Logseq theme plugin with @logseq/libs.
It is based on the current repository structure and these theme-related entry points:
- CSS load order:
tailwind.all.css - Design tokens and theme variables:
packages/ui/src/radix.css,packages/ui/src/radix-hsl.css,packages/ui/src/vars-classic.css,packages/ui/src/colors.css - Shared shui/Radix component CSS:
resources/css/shui.css - CodeMirror theme bridge:
resources/css/codemirror.lsradix.css - Frontend component CSS:
src/main/frontend/**/*.css - Theme runtime attributes:
src/main/frontend/components/theme.cljs,src/main/frontend/state.cljs,src/main/frontend/ui.cljs - Plugin theme registration/injection:
libs/src/LSPlugin.ts,libs/src/LSPlugin.core.ts,libs/src/LSPlugin.user.ts,libs/src/common.ts
Mental model
Logseq themes are mostly CSS-variable based. A theme plugin should first override the semantic variables that the app already consumes, and only then add targeted selector overrides for areas that are not fully tokenized.
CSS loading order
The main stylesheet imports theme foundations before app component CSS:
packages/ui/src/radix.css— Radix color scales as--rx-*variables, for example--rx-gray-01through--rx-gray-12and alpha variants.packages/ui/src/radix-hsl.css— HSL forms such as--rx-gray-01-hslfor Tailwind/shui tokens.packages/ui/src/vars-classic.css— Logseq semantic variables, layout variables, default light/dark values fordata-color=logseq.packages/ui/src/colors.css— accent palettes selected byhtml[data-color=...], mapping--lx-accent-*,--lx-gray-*, shui tokens, and many--ls-*values.packages/ui/src/index.css— Tailwind base.resources/css/shui.css— shared UI components built on shui/Radix/Tailwind tokens.- Third-party CSS: Inter, PhotoSwipe, KaTeX, CodeMirror, PDF.js, Tabler,
codemirror.lsradix.css. src/main/frontend/**/[!_]*.css— component and extension CSS.- A selected plugin theme is injected later as a
<link rel="stylesheet">, so it can override earlier rules when specificity is equal or higher.
Runtime attributes and classes
Theme CSS should scope by attributes on html and compatibility classes on body:
| Hook | Set by | Meaning |
|---|---|---|
html[data-theme="light"] / html[data-theme="dark"] |
frontend.components.theme/container |
Current light/dark mode. Prefer this for theme CSS. |
html.dark |
same component | Tailwind dark-mode hook. |
body.light-theme, body.white-theme, body.dark-theme |
same component | Backward-compatible hooks for older custom CSS/themes. |
html[data-color="logseq"] and other colors |
same component | Accent color selection. A theme plugin can override or ignore this. |
| `html[data-font="serif" | "mono"], html[data-font-global="true"]` |
same component |
Platform classes such as html.is-mobile, html.is-electron, html.is-mac, html.is-native-iphone |
frontend.ui/inject-document-devices-envs! |
Useful for responsive/mobile-safe fixes. |
Plugin theme lifecycle
A theme can be registered in either of two ways:
- Declaratively in
package.json > logseq.themes. During package preparation, Logseq resolves relativeurlvalues to plugin resources, registers each theme, and shows them in the theme selector. - Programmatically with
logseq.provideTheme(theme). This is useful for generated themes or settings-driven variants.
When a user selects a theme, Logseq:
- removes the previously injected custom theme link;
- injects the selected theme CSS link into
document.head; - persists the selected theme separately for
lightanddarkmodes; - updates
:plugin/selected-themeand emits plugin app hooks such as:theme-changed.
Minimal theme plugin
Directory layout
logseq-my-theme/
├── package.json
├── index.html
└── themes/
├── my-theme-light.css
└── my-theme-dark.css
package.json
Use a stable lowercase-kebab logseq.id. For a theme-only plugin, set effect: true and declare each CSS file in themes.
{
"name": "logseq-my-theme",
"version": "0.1.0",
"description": "A custom Logseq theme pack",
"license": "MIT",
"main": "index.html",
"logseq": {
"id": "my-theme",
"main": "index.html",
"title": "My Theme",
"icon": "./icon.png",
"effect": true,
"themes": [
{
"name": "My Theme Light",
"url": "./themes/my-theme-light.css",
"mode": "light",
"description": "Light variant of My Theme"
},
{
"name": "My Theme Dark",
"url": "./themes/my-theme-dark.css",
"mode": "dark",
"description": "Dark variant of My Theme"
}
]
},
"devDependencies": {
"@logseq/libs": "^0.0.17"
}
}
index.html
Declarative themes do not need runtime code, but keeping a tiny entry file makes the plugin package explicit and easy to extend later.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>My Logseq Theme</title>
</head>
<body></body>
</html>
Programmatic variant with provideTheme
If the theme URL or metadata is generated at runtime, use the SDK. Always wait for Logseq to be ready.
import '@logseq/libs'
async function main() {
logseq.provideTheme({
name: 'My Generated Dark Theme',
url: './themes/generated-dark.css',
mode: 'dark',
description: 'Generated from plugin settings',
})
}
logseq.ready(main).catch(console.error)
CSS strategy
Start with variables. Add selector overrides only for UI that cannot be changed via variables.
Dark theme starter
/* themes/my-theme-dark.css */
html[data-theme="dark"] {
color-scheme: dark;
/* Core backgrounds */
--ls-primary-background-color: #111827;
--ls-secondary-background-color: #172033;
--ls-tertiary-background-color: #202b42;
--ls-quaternary-background-color: #2a3650;
--ls-table-tr-even-background-color: var(--ls-secondary-background-color);
--ls-slide-background-color: var(--ls-primary-background-color);
/* Text */
--ls-primary-text-color: #d6deeb;
--ls-secondary-text-color: #eef2ff;
--ls-title-text-color: #f8fafc;
--ls-left-sidebar-text-color: #cbd5e1;
/* Links and refs */
--ls-link-text-color: #7dd3fc;
--ls-link-text-hover-color: #bae6fd;
--ls-link-ref-text-color: var(--ls-link-text-color);
--ls-link-ref-text-hover-color: var(--ls-link-text-hover-color);
--ls-block-ref-link-text-color: #38bdf8;
--ls-tag-text-color: #93c5fd;
--ls-tag-text-hover-color: #bfdbfe;
/* Borders, guidelines, focus */
--ls-border-color: #334155;
--ls-secondary-border-color: #475569;
--ls-tertiary-border-color: rgb(148 163 184 / 0.18);
--ls-guideline-color: rgb(148 163 184 / 0.18);
--ls-focus-ring-color: rgb(56 189 248 / 0.45);
/* Blocks and properties */
--ls-block-properties-background-color: #1e293b;
--ls-page-properties-background-color: #1e293b;
--ls-block-bullet-color: #64748b;
--ls-block-bullet-border-color: #475569;
--ls-block-highlight-color: rgb(14 165 233 / 0.22);
--ls-a-chosen-bg: rgb(56 189 248 / 0.16);
--ls-menu-hover-color: var(--ls-a-chosen-bg);
/* Selection, checkbox, quote, mark, inline code */
--ls-selection-background-color: rgb(56 189 248 / 0.32);
--ls-selection-text-color: #f8fafc;
--ls-page-checkbox-color: #64748b;
--ls-page-checkbox-border-color: #475569;
--ls-page-blockquote-color: var(--ls-primary-text-color);
--ls-page-blockquote-bg-color: #172033;
--ls-page-blockquote-border-color: #38bdf8;
--ls-page-mark-color: #111827;
--ls-page-mark-bg-color: #fde68a;
--ls-page-inline-code-color: #e0f2fe;
--ls-page-inline-code-bg-color: #0f172a;
/* Scrollbars and notifications */
--ls-scrollbar-foreground-color: rgb(148 163 184 / 0.35);
--ls-scrollbar-background-color: rgb(15 23 42 / 0.35);
--ls-scrollbar-thumb-hover-color: rgb(148 163 184 / 0.55);
--ls-notification-background: #1e293b;
--ls-notification-text-color: #f8fafc;
/* shui/Tailwind HSL tokens: use space-separated HSL channels, not hsl(). */
--background: 222 47% 11%;
--foreground: 210 40% 96%;
--card: 222 47% 13%;
--card-foreground: 210 40% 96%;
--popover: 222 47% 10%;
--popover-foreground: 210 40% 96%;
--primary: 199 89% 48%;
--primary-foreground: 210 40% 98%;
--secondary: 217 33% 18%;
--secondary-foreground: 210 40% 96%;
--muted: 217 33% 16%;
--border: 217 33% 24%;
--input: 217 33% 24%;
--ring: 199 89% 48%;
--accent: 199 89% 48%;
--accent-foreground: 210 40% 98%;
}
html[data-theme="dark"] body {
background: var(--ls-primary-background-color);
color: var(--ls-primary-text-color);
}
html[data-theme="dark"] ::selection {
background: var(--ls-selection-background-color);
color: var(--ls-selection-text-color);
}
Light theme starter
/* themes/my-theme-light.css */
html[data-theme="light"] {
color-scheme: light;
--ls-primary-background-color: #ffffff;
--ls-secondary-background-color: #f8fafc;
--ls-tertiary-background-color: #eef2f7;
--ls-quaternary-background-color: #e2e8f0;
--ls-primary-text-color: #1e293b;
--ls-secondary-text-color: #0f172a;
--ls-title-text-color: #0f172a;
--ls-left-sidebar-text-color: #334155;
--ls-link-text-color: #0369a1;
--ls-link-text-hover-color: #075985;
--ls-link-ref-text-color: var(--ls-link-text-color);
--ls-link-ref-text-hover-color: var(--ls-link-text-hover-color);
--ls-block-ref-link-text-color: #0284c7;
--ls-tag-text-color: #0369a1;
--ls-tag-text-hover-color: #075985;
--ls-border-color: #cbd5e1;
--ls-secondary-border-color: #e2e8f0;
--ls-tertiary-border-color: rgb(15 23 42 / 0.08);
--ls-guideline-color: rgb(15 23 42 / 0.08);
--ls-focus-ring-color: rgb(14 165 233 / 0.35);
--ls-block-properties-background-color: #f1f5f9;
--ls-page-properties-background-color: #f1f5f9;
--ls-block-bullet-color: #94a3b8;
--ls-block-bullet-border-color: #cbd5e1;
--ls-block-highlight-color: #e0f2fe;
--ls-a-chosen-bg: #e0f2fe;
--ls-menu-hover-color: var(--ls-a-chosen-bg);
--ls-selection-background-color: #dbeafe;
--ls-selection-text-color: #0f172a;
--ls-page-checkbox-color: #94a3b8;
--ls-page-checkbox-border-color: #94a3b8;
--ls-page-blockquote-color: var(--ls-primary-text-color);
--ls-page-blockquote-bg-color: #f8fafc;
--ls-page-blockquote-border-color: #38bdf8;
--ls-page-mark-color: #0f172a;
--ls-page-mark-bg-color: #fef3c7;
--ls-page-inline-code-color: #0f172a;
--ls-page-inline-code-bg-color: #f1f5f9;
--ls-scrollbar-foreground-color: rgb(15 23 42 / 0.12);
--ls-scrollbar-background-color: rgb(15 23 42 / 0.05);
--ls-scrollbar-thumb-hover-color: rgb(15 23 42 / 0.22);
--background: 0 0% 100%;
--foreground: 222 47% 11%;
--card: 0 0% 100%;
--card-foreground: 222 47% 11%;
--popover: 0 0% 100%;
--popover-foreground: 222 47% 11%;
--primary: 199 89% 40%;
--primary-foreground: 0 0% 100%;
--secondary: 210 40% 96%;
--secondary-foreground: 222 47% 11%;
--muted: 210 40% 96%;
--border: 214 32% 91%;
--input: 214 32% 91%;
--ring: 199 89% 40%;
--accent: 199 89% 40%;
--accent-foreground: 0 0% 100%;
}
Important variable groups
Prefer these variables before reaching for selectors.
Global layout and typography
| Variable | Purpose |
|---|---|
--ls-page-text-size |
Base page text size under #root. |
--ls-page-title-size |
Page title size. Mobile may override title layout. |
--ls-main-content-max-width, --ls-main-content-max-width-wide |
Main content width. |
--ls-font-family |
Global app font when not overridden by user font settings. |
--ls-scrollbar-width |
Custom scrollbar width. |
--ls-border-radius-low, --ls-border-radius-medium |
Classic radius tokens. |
--ls-headbar-height, --ls-headbar-inner-top-padding |
Header sizing. |
--ls-left-sidebar-width, --ls-left-sidebar-sm-width, --ls-left-sidebar-nav-btn-size |
Left sidebar sizing. |
Logseq semantic colors
| Variable | Purpose |
|---|---|
--ls-primary-background-color |
Main background. |
--ls-secondary-background-color |
Secondary surfaces, editors, menus. |
--ls-tertiary-background-color |
Nested/raised surfaces. |
--ls-quaternary-background-color |
Active/hover surfaces. |
--ls-primary-text-color, --ls-secondary-text-color, --ls-title-text-color |
Body, stronger text, titles. |
--ls-border-color, --ls-secondary-border-color, --ls-tertiary-border-color |
Borders. |
--ls-guideline-color |
Block indentation guide lines. |
--ls-active-primary-color, --ls-active-secondary-color |
Active states. |
--ls-a-chosen-bg, --ls-menu-hover-color |
Chosen menu/list item backgrounds. |
--ls-focus-ring-color |
Focus rings. |
Links, references, tags, and blocks
| Variable | Purpose |
|---|---|
--ls-link-text-color, --ls-link-text-hover-color |
General links. |
--ls-link-ref-text-color, --ls-link-ref-text-hover-color |
Page references. |
--ls-block-ref-link-text-color |
Block references. |
--ls-tag-text-color, --ls-tag-text-hover-color, --ls-tag-text-opacity, --ls-tag-text-hover-opacity |
Tags. |
--ls-block-bullet-color, --ls-block-bullet-border-color, --ls-block-bullet-active-color |
Block bullets. |
--ls-block-highlight-color |
Block highlight. |
--ls-block-properties-background-color, --ls-page-properties-background-color |
Property panels. |
Content tokens
| Variable | Purpose |
|---|---|
--ls-selection-background-color, --ls-selection-text-color |
Text selection. |
--ls-page-checkbox-color, --ls-page-checkbox-border-color |
Markdown task checkboxes/radios. |
--ls-page-blockquote-color, --ls-page-blockquote-bg-color, --ls-page-blockquote-border-color |
Blockquotes. |
--ls-page-mark-color, --ls-page-mark-bg-color |
Highlight/mark text. |
--ls-page-inline-code-color, --ls-page-inline-code-bg-color |
Inline code. |
--ls-table-tr-even-background-color |
Alternating table rows. |
--ls-cloze-text-color |
Cloze text. |
--ls-slide-background-color |
Slide mode background. |
shui/Tailwind HSL tokens
Newer shared UI components use HSL channel tokens, often through Tailwind classes such as bg-background, bg-popover, border, text-foreground, and bg-primary.
Set these as space-separated HSL channels:
html[data-theme="dark"] {
--background: 222 47% 11%;
--foreground: 210 40% 96%;
--card: 222 47% 13%;
--card-foreground: 210 40% 96%;
--popover: 222 47% 10%;
--popover-foreground: 210 40% 96%;
--primary: 199 89% 48%;
--primary-foreground: 210 40% 98%;
--secondary: 217 33% 18%;
--secondary-foreground: 210 40% 96%;
--muted: 217 33% 16%;
--border: 217 33% 24%;
--input: 217 33% 24%;
--ring: 199 89% 48%;
--accent: 199 89% 48%;
--accent-foreground: 210 40% 98%;
}
Do not write --primary: hsl(199 89% 48%); consumers call hsl(var(--primary)).
--lx-* and --rx-* color scales
--rx-*variables are Radix-style raw palettes. They are broad and already available.--lx-gray-01through--lx-gray-12and--lx-gray-*-alpharepresent the active neutral scale.--lx-accent-01through--lx-accent-12and--lx-accent-*-alpharepresent the active accent scale.
If your theme is a complete palette, map --lx-gray-* and --lx-accent-* as well as --ls-*. This makes Radix/shui-heavy UI more consistent:
html[data-theme="dark"] {
--lx-gray-01: #0f172a;
--lx-gray-02: #111827;
--lx-gray-03: #1e293b;
--lx-gray-04: #273449;
--lx-gray-05: #334155;
--lx-gray-06: #475569;
--lx-gray-07: #64748b;
--lx-gray-08: #94a3b8;
--lx-gray-09: #cbd5e1;
--lx-gray-10: #dbeafe;
--lx-gray-11: #e2e8f0;
--lx-gray-12: #f8fafc;
--lx-accent-09: #0ea5e9;
--lx-accent-10: #0284c7;
--lx-accent-11: #38bdf8;
--lx-accent-12: #e0f2fe;
}
UI selector map
Use this as a last-mile map after variables. Keep overrides narrow and prefer :where(...) to avoid specificity wars.
App shell and layout
| Area | Useful selectors | Notes |
|---|---|---|
| Root/app | #root, #app-container, #root-container.theme-container, main.theme-container-inner |
#root uses --ls-page-text-size; main.theme-container-inner defines --left-sidebar-bg-color. |
| Main content | #main-container, #main-content, #main-content-container, .page-blocks-inner |
Avoid hard-coded viewport hacks; test desktop and mobile. |
| Header | .cp__header, .head, .button, .ui__button |
Mostly tokenized through shui/Tailwind variables. |
| Left sidebar | .left-sidebar-inner, .sidebar-header-container, .sidebar-contents-container, .sidebar-content-group, .sidebar-navigations |
Prefer --left-sidebar-bg-color, --ls-left-sidebar-text-color, and --ls-left-sidebar-width. |
| Right sidebar | #right-sidebar, .cp__right-sidebar-inner, .sidebar-item, .references-blocks-item |
Nested reference cards may need extra contrast. |
Pages, blocks, and editor
| Area | Useful selectors | Notes |
|---|---|---|
| Page title | .ls-page-title, .page-title, .journal-title, .page-title-sizer-wrapper |
Prefer --ls-page-title-size and title text variables. |
| Blocks | .ls-block, .block-main-container, .block-content-wrapper, .block-content, .block-body |
Avoid altering core layout unless necessary. |
| Block tree guides | .block-children-container, .block-children, .block-children-left-border |
Governed by --ls-guideline-color and related border variables. |
| Bullets/control | .block-control-wrap, .block-control, .bullet-container, .bullet-link-wrap |
Governed by bullet variables; keep hit areas accessible. |
| Page refs | .page-ref, .page-reference, .breadcrumb |
Use link/reference variables first. |
| Block refs | .block-ref, .block-ref-no-title, .open-block-ref-link |
Use --ls-block-ref-link-text-color, property surface variables. |
| Properties | .block-properties, .page-properties, .property-value-inner, .property-key |
Use property background variables. |
| Editor textarea | .editor-inner textarea, .edit-input, .non-block-editor textarea |
Background usually comes from --ls-secondary-background-color. |
| Autocomplete/slash popup | #ui__ac-inner, .menu-link, .absolute-modal[data-modal-name], .cp__commands-slash |
Use menu hover and popover tokens. |
Shared UI, dialogs, command palette
| Area | Useful selectors | Notes |
|---|---|---|
| Buttons | .ui__button, .button, .ui__toggle, .ui__toggle-background-on, .ui__toggle-background-off |
Set --primary, --primary-foreground, --accent, --ring. |
| Dialogs/modals | .ui__modal, .ui__modal-panel, .ui__dialog-overlay, .ui__dialog-content, .ui__alert-dialog-content |
--ls-modal-overlay-gradient-start/end can control modal overlays. |
| Dropdowns/popovers | .ui__dropdown-menu-content, .ui__popover-content, div[data-radix-menu-content], div[data-radix-popper-content-wrapper] |
Usually driven by --popover, --border, --accent, --lx-popover-bg. |
| Select/calendar | .ui__select-content, .ui__calendar, .rc-datepicker |
shui tokens and --accent matter. |
| Notifications | .ui__notifications .notification-area |
Use --ls-notification-background, --ls-notification-text-color. |
| Command palette | .cp__cmdk |
Border and hint colors use --ls-border-color, --lx-gray-*, --accent. |
| Context menu | #custom-context-menu, .ls-context-menu-content, .menu-links-wrapper |
Popover/menu variables usually cover it. |
Extensions and special surfaces
| Area | Useful selectors/variables | Notes |
|---|---|---|
| CodeMirror | .cm-s-lsradix, .cm-s-lsradix.cm-s-dark, .cm-s-lsradix.cm-s-light, .CodeMirror-*, .cm-* |
resources/css/codemirror.lsradix.css already bridges many --lx-*/--ls-* variables. Override only syntax colors if needed. |
.extensions__pdf-container, .extensions__pdf-toolbar, .extensions__pdf-outline, --ph-highlight-color-*, --ph-link-color, --ph-view-container-width, --lx-pdf-container-dark-bg |
PDF highlight colors are --ph-*, not --ls-highlight-color-*. |
|
| Graph | .graph-layout, .graph-filters, graph extension CSS under src/main/frontend/extensions/graph.css |
Some graph styles are canvas/SVG-driven; inspect DOM before overriding. |
| Whiteboard/tldraw | .tl-container, .tl-button, --ls-wb-stroke-color-default, --ls-wb-background-color-default, --ls-wb-text-color-default |
Accent colors in colors.css set default whiteboard tokens. |
| Tables | .table-wrapper, .table-auto, table rows |
Start with --ls-table-tr-even-background-color, borders, text tokens. |
| Cards/flashcards | .ls-card, .ui__dialog-content[label=flashcards__cp] |
Check dialog contrast and card minimum sizes. |
| Plugins UI | .cp__plugins-page, .cp__themes-installed |
Useful when styling the installed theme picker itself. |
Scoped overrides: examples
Keep overrides mode-specific
html[data-theme="dark"] :where(.cp__right-sidebar-inner .references-blocks-item) {
background: color-mix(in srgb, var(--ls-secondary-background-color) 88%, white 12%);
}
Prefer low specificity for optional polish
html[data-theme="light"] :where(.page-reference:hover) {
background: rgb(14 165 233 / 0.12);
}
Avoid global resets
Avoid rules like this in a theme plugin:
/* Avoid */
* {
transition: all 200ms ease;
}
They can slow editing, affect CodeMirror/PDF.js, and break subtle interaction states.
Mobile and desktop considerations
- Test
html.is-mobile,html.is-native-iphone,html.is-native-android, andhtml.is-electronlayouts if your theme changes spacing or header/sidebar dimensions. - Respect safe-area insets in mobile-specific layout. Existing CSS uses
env(safe-area-inset-*)in some places. - Avoid changing
-webkit-app-regionon Electron headers/PDF toolbars unless you are fixing a drag-region issue. - Do not shrink block control or bullet hit areas below the existing touch-friendly sizes.
- Avoid fixed pixel widths for the main content unless they are variables such as
--ls-main-content-max-width.
Assets and fonts
Prefer local assets packaged with the plugin. Relative URLs inside the selected theme CSS are resolved relative to the CSS file by the browser.
@font-face {
font-family: "MyThemeText";
src: url("../fonts/MyThemeText.woff2") format("woff2");
font-display: swap;
}
html[data-theme="dark"] {
--ls-font-family: "MyThemeText", Inter, ui-sans-serif, system-ui, sans-serif;
}
Be careful with remote @import URLs: they can slow startup, fail offline, and leak network requests from a local-first app.
Debugging workflow
- Enable Logseq Developer mode.
- Load the unpacked theme plugin.
- Open DevTools and inspect
document.documentElement:data-themedata-colordata-font- platform classes
- In the theme selector, choose each declared light/dark theme.
- Verify the selected CSS appears as a
<link rel="stylesheet">indocument.head. - Inspect computed variables on
html,body,.theme-container, and the specific UI area you are styling. - If a variable is not taking effect, check whether the component is using
--lx-*, shui HSL tokens, or a targeted class instead of--ls-*.
Validation checklist
Before publishing a theme plugin, test at least these screens and states:
- Light and dark mode selection; switching between modes keeps each custom theme selection.
- Default accent color and at least one non-default
data-coloraccent. - Page title, journal title, normal blocks, nested blocks, block refs, page refs, tags, properties.
- Editing state: textarea, slash command menu, autocomplete, date picker, block context menu.
- Left sidebar, right sidebar, search, command palette, settings dialogs.
- Notifications, confirmation dialogs, dropdowns, popovers, tooltips.
- Code blocks / CodeMirror, inline code, marks, blockquotes, tables.
- PDF viewer highlights and toolbar if PDF is in scope.
- Graph and whiteboard/tldraw surfaces if your palette changes accents broadly.
- Desktop Electron and at least one mobile/narrow viewport if the theme changes layout/spacing.
- High contrast of text against every background; visible focus rings; visible selected/hover states.
- Reduced-motion friendliness if you add animations.
Common pitfalls
- Overriding only
--ls-*while newer shui components still read--background,--popover,--border,--primary, or--lx-*variables. - Writing HSL tokens as full CSS colors instead of channel values.
- Using high-specificity selectors or
!importanteverywhere, making user custom CSS and future Logseq changes harder to coexist with. - Styling by generated Tailwind class names instead of stable Logseq/Radix selectors and variables.
- Forgetting to scope a dark theme to
html[data-theme="dark"]or a light theme tohtml[data-theme="light"]. - Hard-coding external font/image URLs without offline fallback.
- Changing editor layout, block bullets, or mobile sidebars without testing touch interactions.
Quick reference: priority order for theme authors
- Set
--ls-*semantic variables for Logseq-specific UI. - Set shui/Tailwind HSL tokens (
--background,--foreground,--popover,--border,--primary, etc.). - Map
--lx-gray-*and--lx-accent-*for newer shared UI consistency. - Add extension variables such as
--ph-*and--ls-wb-*only if your theme covers PDF/whiteboard. - Add low-specificity, mode-scoped selector overrides for the remaining gaps.