Project Overview
Custom Theme Studio is a desktop-only Obsidian plugin that enables users to create, customize, and export themes through an integrated CSS editor. It leverages Obsidian’s built-in CSS variables (obsidian.css), provides live previews, and packages themes for sharing.
Main Capabilities
- Interactive CSS editor with live preview and variable management
- Built-in support for light and dark modes
- One-click export of complete theme packages (CSS + manifest)
- Import and refine existing themes
Problems It Solves
- Eliminates manual theme file editing and reloads
- Ensures consistent styling across modes and UI components
- Simplifies versioning and distribution of themes
- Lowers barrier for new theme authors
Core Components
- manifest.json: Plugin metadata (id:
custom-theme-studio
, version:1.0.1
, desktop-only) - obsidian.css: Reference for Obsidian’s CSS variables, layout tokens, and UI components
- package.json: Development scripts, dependencies, and build configuration
Sample manifest.json Snippet
{
"id": "custom-theme-studio",
"name": "Custom Theme Studio",
"version": "1.0.1",
"minAppVersion": "0.12.0",
"author": "Your Name",
"description": "Create, customize, and export Obsidian themes via CSS editor.",
"isDesktopOnly": true
}
Example CSS Customization
/* Change primary accent color */
:root {
--text-accent: #ff75a0;
}
/* Dark-mode background override */
.theme-custom.theme-dark {
--background-primary: #1e1e2e;
}
## Getting Started
This guide covers installing Custom Theme Studio in Obsidian—either from the Community Plugin directory or by building from source—and walking through your first theme tweak.
### Prerequisites
- Obsidian v1.4+
- Node.js v14+ (for local builds)
- A vault with Community Plugins enabled
### Installation via Obsidian Community Plugins
1. In Obsidian, open Settings → Community Plugins.
2. Turn off Safe Mode if enabled.
3. Browse Community Plugins → Search for “Custom Theme Studio”.
4. Click “Install” → “Enable”.
Custom Theme Studio now appears in the command palette and as a sidebar icon.
### Building from Source
Follow these steps to customize or contribute to the plugin.
#### 1. Clone and Install Dependencies
```bash
git clone https://github.com/gapmiss/custom-theme-studio.git
cd custom-theme-studio
npm install
2. Development Mode (Live Rebuild)
npm run dev
- Runs esbuild in watch mode.
- On each save, esbuild rebuilds
main.js
. - Reload Obsidian or use the Reload command (Ctrl/Cmd+R) to pick up changes.
3. Production Build
npm run build
- Outputs a production bundle in
dist/
. - Exits on completion.
4. Install Locally in Your Vault
Create a plugin folder in your vault:
- macOS/Linux:
mkdir -p ~/YourVault/.obsidian/plugins/custom-theme-studio
- Windows (PowerShell):
New-Item -ItemType Directory "$env:USERPROFILE\YourVault\.obsidian\plugins\custom-theme-studio"
- macOS/Linux:
Copy build artifacts:
cp dist/main.js manifest.json ~/YourVault/.obsidian/plugins/custom-theme-studio/
In Obsidian, open Settings → Community Plugins → Disabled Plugins → Enable “Custom Theme Studio”.
First Theme Tweak
Open the command palette (Ctrl/Cmd+P), run Open Custom Theme Studio.
In the CSS editor panel, add a rule. For example, to change heading colors:
/* Change H1 color in preview */ .markdown-preview-view h1 { color: #e91e63; }
Click Save & Apply. Observe the preview update immediately.
When satisfied, export your theme via the “Export Theme” button.
You’re now ready to explore advanced customization and share your themes!
Using Custom Theme Studio
Customize your Obsidian theme day-to-day by editing variables, adding CSS rules, picking UI elements by clicking, live-previewing styles, and exporting or importing your theme bundle.
Managing Custom CSS Variables (CSSVariableManager.updateVariable
)
Handle adding, updating, or removing CSS variables in plugin settings and persist changes.
SignatureupdateVariable(name: string, value: string, parent: string): void
Parameters
name
– CSS variable name (e.g."--accent-h"
).value
– New value (e.g."300"
), or""
to remove override.parent
– Variable category (e.g."colors"
).
Behavior
- Reads
plugin.settings.customVariables: CSSVariable[]
. - Finds matching entry by
variable === name
andparent === parent
.- If found &
value !== ''
: updatesentry.value
. - If found &
value === ''
: removes entry.
- If found &
- If not found &
value !== ''
: pushes new{ parent, variable: name, value }
. - Calls
plugin.saveSettings()
.
Code Example
import { CSSVariableManager } from 'src/managers/cssVariableManager';
const cssVarManager = new CSSVariableManager(plugin);
const input = document.createElement('input');
input.type = 'text';
input.placeholder = 'Enter --accent-h value';
input.addEventListener('input', (e) => {
const newValue = (e.target as HTMLInputElement).value;
cssVarManager.updateVariable('--accent-h', newValue, 'colors');
// Live-apply if theme is enabled
if (plugin.settings.themeEnabled) {
plugin.themeManager.applyCustomTheme();
}
});
containerEl.appendChild(input);
Usage Patterns
- Add/Change: pass non-empty
value
. - Reset: pass
""
to remove override. - All calls persist automatically; theme updates via
themeManager.applyCustomTheme()
.
Editing Custom CSS Elements (CSSEditor
)
Create and manage CSS rules for specific selectors, live-preview them, and save to settings.
Core Methods
createEditorSection(container: HTMLElement): void
setSelector(selector: string, focus?: boolean): void
applyChanges(): void
saveElement(element: CSSElement): void
Code Example
import CSSEditor, { CSSElement } from 'src/managers/cssEditor';
// Initialize editor in your view
const cssEditor = new CSSEditor(plugin, view);
cssEditor.createEditorSection(plugin.settingsContainerEl);
// Pre-fill selector (e.g. from last pick)
cssEditor.setSelector(plugin.settings.lastSelectedSelector, true);
// Programmatically add a new rule
const newRule: CSSElement = {
id: 'rule-1',
selector: '.modal-dialog',
css: 'background-color: var(--background-primary); padding: 8px;',
enabled: true
};
cssEditor.saveElement(newRule);
// Persist and live-preview changes
cssEditor.applyChanges();
Inline Editing
The editor integrates CodeMirror. User edits CSS in the UI, and changes apply on each keystroke. Calling applyChanges()
writes plugin.settings.customCSSElements
and re-renders theme.
Interactive Element Picker (ElementSelector
)
Point-and-click any Obsidian UI element to auto-populate the CSS editor with a robust selector.
Flow
- User clicks “Select element” button in your view.
elementSelector.startElementSelection()
attaches global mouse handlers, shows tooltip, highlights on hover.- On click:
generateSelector(el, evt.altKey)
returns default or specific selector.- Saves to
plugin.settings.lastSelectedSelector
, callsplugin.saveSettings()
. - Injects into
cssEditor
viasetSelector()
andshowEditorSection(true)
.
Key Snippets
// Start selection
selectBtn.addEventListener('click', () => {
cssEditor.resetEditor();
cssEditor.showEditorSection(false);
elementSelector.startElementSelection();
});
// Generate selector
generateSelector(el: HTMLElement, useSpecific = false): string {
const tag = el.tagName.toLowerCase();
if (el.id) return `${tag}#${el.id}`;
if (!useSpecific && el.hasAttribute('aria-label')) {
const v = this.escapeAttributeValue(el.getAttribute('aria-label')!);
return `${tag}[aria-label="${v}"]`;
}
// fallback: tag + classes, data-*, etc.
return `${tag}.${[...el.classList].join('.')}`;
}
// On element click
selectElement(el: HTMLElement, evt: MouseEvent) {
const selector = this.generateSelector(el, evt.altKey);
this.plugin.settings.lastSelectedSelector = selector;
this.plugin.saveSettings();
this.view.cssEditor.setSelector(selector, false);
this.view.cssEditor.showEditorSection(true);
}
Tips
- Hold Alt for full-specific selector (classes, data-attributes).
- Default picks use
aria-label
or shortestdata-*
. - Styles:
.cts-element-picker-active
on<body>
.cts-element-picker-hover
on hovered element.cts-element-picker-tooltip
for info box
Live Previewing and Applying Changes
Both variable updates and CSS element edits apply instantly:
- Variables:
CSSVariableManager.updateVariable()
callsplugin.saveSettings()
. Then callthemeManager.applyCustomTheme()
to inject updated CSS variables. - Elements:
CSSEditor
applies inline styles as you type. CallapplyChanges()
to persist and rebuild the theme bundle.
// After multiple edits
await plugin.themeManager.applyCustomTheme();
showNotice('Theme updated', 'success');
Exporting and Importing Themes
Exporting
Use ThemeManager
to bundle CSS and manifest for distribution:
exportThemeCSS(): Promise<void>
• Gathers CSS variables & custom CSS.
• Prepends header comments (exportThemeName
,exportAuthor
,exportURL
).
• Formats via Prettier ifexportPrettierFormat
is true.
• Triggers download oftheme.css
.exportThemeManifest(): void
• Buildsmanifest.json
with kebab-cased name, version, author, URLs.
• Triggers download.copyThemeToClipboard(): Promise<void>
• Generates CSS bundle, formats, copies to clipboard.copyManifestToClipboard(): void
• Builds JSON, copies to clipboard.
Example
// In a button handler:
await plugin.themeManager.exportThemeCSS();
plugin.themeManager.exportThemeManifest();
await plugin.themeManager.copyThemeToClipboard();
plugin.themeManager.copyManifestToClipboard();
Importing Configuration
Restore your complete Custom Theme Studio settings:
- Open plugin Settings → Import Configuration.
- Select a JSON file exported earlier.
- Plugin loads settings, calls
plugin.saveSettings()
, and re-renders the view and theme.
You can also import programmatically:
// Pseudo-code
import { readFileSync } from 'fs';
const data = JSON.parse(readFileSync('cts-config.json', 'utf-8'));
plugin.settings = data;
await plugin.saveSettings();
await plugin.themeManager.applyCustomTheme();
Tips
- Ensure
exportThemeName
,exportAuthor
,exportURL
, and formatting options are set before export. - Use import/export buttons in the Settings UI for one-click operations.
Configuration & Settings
This section lists every option exposed in the Settings tab, explains how they persist in Obsidian, and shows how to import/export and validate configurations.
Settings Schema
Import the core interface and defaults from the settings module:
import {
CustomThemeStudioSettings,
DEFAULT_SETTINGS
} from '../settings';
import { ICodeEditorConfig } from '../interfaces/types';
Properties on CustomThemeStudioSettings
:
• enableLivePreview (boolean)
Toggle live CSS preview as you edit variables or custom selectors.
• darkModeOnly (boolean)
Apply your custom theme only when Obsidian is in dark mode.
• globalCSS (boolean)
Append generated CSS to the app’s global stylesheet vs. only the workspace pane.
• editorConfig (ICodeEditorConfig)
Configure the integrated Ace editor (theme, keyboard, font, line numbers, wrap).
• exportScope ('workspace' | 'vault')
Choose to export CSS to your workspace folder or vault root.
• includeComments (boolean)
Include variable names and section comments in the exported CSS file.
• cssVariables (Record<string, string>)
Map of CSS variable names to their current values.
• enabledElements (string[])
List of CSS selectors or component IDs you’ve toggled on/off for export.
Accessing and Updating Settings
Load your settings on plugin startup and register the Settings tab:
// main.ts
export default class CustomThemeStudio extends Plugin {
settings: CustomThemeStudioSettings;
async onload() {
this.settings = Object.assign(
{},
DEFAULT_SETTINGS,
await this.loadData()
);
this.addSettingTab(
new CustomThemeStudioSettingTab(this.app, this)
);
}
// Save whenever you mutate this.settings
async saveSettings() {
await this.saveData(this.settings);
}
}
Update a setting programmatically:
// Disable live preview
this.settings.enableLivePreview = false;
await this.saveSettings();
// Refresh your UI or apply changes here...
Importing and Exporting Settings
Leverage settingsIO
for cross-platform backup and restore:
import settingsIO from './settings/settingsIO';
// Export current settings
await settingsIO.exportSettings(this.settings, this.app);
// Import and validate
const imported = await settingsIO.importSettings(this.app);
if (imported) {
// Replace, persist, and refresh
this.settings = imported;
await this.saveData(imported);
this.settingTab.reload();
}
Key points:
- Electron builds prompt a native file-save/open dialog.
- Mobile/web fall back to reading/writing
CTS_settings.json
in vault root. settingsIO.validateSettings(obj)
ensures structure before acceptance.- Success/failure notices surface automatically via Obsidian’s notice API.
Resetting to Defaults
Reset all customizations back to the shipped defaults:
import { DEFAULT_SETTINGS } from '../settings';
// Reset
this.settings = { ...DEFAULT_SETTINGS };
await this.saveSettings();
this.settingTab.reload();
new Notice('Custom Theme Studio settings reset.');
Runtime Validation
If you dynamically load external JSON, guard against invalid shapes:
import settingsIO from './settings/settingsIO';
async function loadExternalConfig(path: string) {
const raw = await vaultAdapter.read(path);
const data = JSON.parse(raw);
if (settingsIO.validateSettings(data)) {
this.settings = data;
await this.saveSettings();
} else {
new Notice('Invalid Custom Theme Studio configuration.');
}
}
Use these patterns to ensure your plugin always operates on a valid, up-to-date settings object.
Development & Contribution Guide
This guide covers cloning, environment setup, coding standards, building, testing, and releasing new versions to GitHub and the Obsidian Community Plugin registry.
Prerequisites
- Node.js v16+ and npm
- Git
- Obsidian (for manual plugin testing)
1. Clone & Setup
git clone https://github.com/gapmiss/custom-theme-studio.git
cd custom-theme-studio
npm install
2. Coding Standards
EditorConfig
.editorconfig
enforces UTF-8, LF line endings, final newline, tabs of width 4:
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
indent_style = tab
indent_size = 4
tab_width = 4
ESLint
.eslintrc
extends recommended rules for TypeScript; .eslintignore
skips node_modules/
and main.js
.
Install and configure:
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin
Add to package.json
:
"scripts": {
"lint": "eslint \"src/**/*.ts\""
}
Run linting:
npm run lint
3. Building & Development
esbuild Configuration
esbuild.config.mjs
bundles src/main.ts
into main.js
, excludes Obsidian/Electron APIs, supports watch mode in development and optimized one-time build in production.
npm Scripts
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production"
}
npm run dev
Starts a watcher; rebuildsmain.js
on anysrc/**/*.ts
change.npm run build
Runs type-check then bundles optimized output.
To add new peer-dependencies or APIs (e.g. Obsidian/Electron) to the bundle exclusion, update the
external
array inesbuild.config.mjs
.
4. Type Checking
tsconfig.json
enables strict mode, ESNext modules, source maps.
Run TypeScript checking separately:
"scripts": {
"typecheck": "tsc -noEmit"
}
npm run typecheck
5. Release Workflow
Scripts in package.json
"scripts": {
"release": "node release.mjs",
"release:minor": "node release.mjs minor",
"release:major": "node release.mjs major",
"version": "node version-bump.mjs"
}
Creating a Release
# Patch release
npm run release
# Minor release
npm run release:minor
# Major release
npm run release:major
This performs:
- Git cleanliness check
- Bumps
package.json
version - Syncs
manifest.json
andversions.json
- Runs build and type-check
- Commits artifacts (
main.js
,manifest.json
,styles.css
) - Tags commit (
vX.Y.Z
) and pushes - (Optional) Creates GitHub Release via
gh release create
On failure, it rolls back version changes and PRs. For manual version sync without full release, use:
npm run version
6. Publishing to Obsidian Community Plugins
- Confirm
manifest.json
fields:id
,name
,version
,minAppVersion
. - Push tagged release to GitHub.
- Open a pull request against the Obsidian Community Plugins repo, updating the plugin entry with the new
manifest.json
. - Once merged, your version appears in Obsidian’s Community Plugins browser.
By following these steps, contributors maintain consistent code style, streamline development, and automate releases for seamless updates.