Using it in Electron
Install videorc/ui into an Electron renderer built with Vite. The aliases, the window's background, platform glyphs, accelerators, and keeping your copy in sync.
Videorc runs these components in Electron, in a React renderer built with Vite and rsc: false. Nothing in the registry imports next/* or Electron, so the same steps work for electron-vite, Electron Forge's Vite plugin or a plain Vite app.
Set up the renderer
Point components.json at the renderer
Paths and aliases are relative to the renderer's source. With electron-vite's layout:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "base-rhea",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/renderer/src/styles.css",
"baseColor": "stone",
"cssVariables": true
},
"iconLibrary": "phosphor",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@videorc": "https://ui.videorc.com/r/{name}.json"
}
}Resolve the @ alias
Vite resolves it in the renderer's config:
import { resolve } from "node:path";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { defineConfig } from "electron-vite";
export default defineConfig({
// main and preload stay as they are
renderer: {
plugins: [react(), tailwindcss()],
resolve: {
alias: { "@": resolve("src/renderer/src") },
},
},
});TypeScript resolves it through paths. Add them to the renderer's tsconfig and to the root tsconfig.json, which the shadcn CLI reads:
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/renderer/src/*"]
}
}
}Import shadcn's variants
The renderer's stylesheet needs shadcn/tailwind.css after Tailwind. The theme lands below these lines.
@import "tailwindcss";
@import "tw-animate-css";
@import "shadcn/tailwind.css";
@custom-variant dark (&:is(.dark *));Add the theme and the desktop base
$ pnpm dlx shadcn@latest add @videorc/theme @videorc/desktopThe desktop base makes the window behave like an app: chrome text doesn't select, the arrow stays the pointer, and nothing rubber-bands.
About "use client"
Registry files that run in the browser start with "use client", and the CLI keeps the directive even for rsc: false projects. Vite ignores it, so it's harmless. Leave it in: a file that matches the registry exactly diffs cleanly the next time you sync.
The window's background
Videorc's surfaces are translucent coats that composite to the theme's solid. The window itself is opaque and paints that solid. There's no vibrancy, no Mica and no transparent: true, so the desktop never shows through and nothing depends on the wallpaper or the compositor.
Give the window the theme's solid as its background, so the first frame is already the right tone:
const mainWindow = new BrowserWindow({
// --glass-solid: #070707 in dark, #FAFAFA in light
backgroundColor: nativeTheme.shouldUseDarkColors ? "#070707" : "#FAFAFA",
});Don't reach for backdrop-filter to add a frost. On a window with a native material, a CSS backdrop blur shows in Chromium's own screenshots but never reaches the display.
The theme sets color-scheme on body. Don't set it inline on <html>: a root color-scheme: light makes Chromium paint an opaque canvas over the window.
Platform glyphs
Shortcuts and OS words depend on the platform: ⌘ or Ctrl, Finder or Explorer, Trash or Recycle Bin. Platform works it out in this order:
- the platform you pass, such as Electron's
process.platform; data-platformon<html>;- the user agent.
Set data-platform before the first paint, so a Mac never flashes "Ctrl". Use a small script file in the <head>, not a module, so it runs before the page draws. A file also passes a strict Content Security Policy where an inline script wouldn't.
const agent = navigator.userAgent;
let platform = "linux";
if (agent.includes("Macintosh")) {
platform = "darwin";
} else if (agent.includes("Windows")) {
platform = "win32";
}
document.documentElement.dataset.platform = platform;<head>
<script src="/platform.js"></script>
</head>When your app knows the platform for sure (from process.platform, passed through your preload), pass it in: <ShortcutKeys accelerator="CmdOrCtrl+K" platform={platform} />.
The theme can't ship a custom variant, so add one yourself for Windows-only chrome. It keys on the same attribute, so it never pops in:
@custom-variant win32 (&:is([data-platform="win32"] *));<div className="hidden win32:flex">…</div>Accelerators
Write every shortcut once, as an Electron accelerator, the same string you give a menu item or globalShortcut:
const streamManagerItem: MenuItemConstructorOptions = {
accelerator: "CmdOrCtrl+Shift+J",
click: openStreamManager,
label: "Stream Manager",
};Then print it in the renderer with ShortcutKeys:
<ShortcutKeys accelerator="CmdOrCtrl+Shift+J" />That reads ⇧⌘J on macOS and Ctrl+Shift+J on Windows and Linux, the way each platform's menus print it. In running text, use shortcutLabel from platform, and shortcutAriaKeys for aria-keyshortcuts. Keep the accelerators in one table that main and the renderer both import, so a menu and its key chip can't disagree. Accelerator parses every spelling Electron accepts.
Electron state comes in through props
Registry components never call window.* bridges, ipcRenderer or anything else Electron. Whatever Electron knows (the platform, whether a window is pinned, the list of cameras) comes in as props, and changes go out as callbacks. Your screens talk to the preload API:
function KeepOnTopButton() {
const [pinned, setPinned] = useState(false);
const togglePinned = () => {
// The screen talks to Electron; the button only sees a prop and a callback.
window.desktop.setAlwaysOnTop(!pinned);
setPinned(!pinned);
};
return (
<Button aria-pressed={pinned} onClick={togglePinned} variant="ghost">
Keep on top
</Button>
);
}Here window.desktop stands for your own preload API. Keeping it out of the components means they render the same in a browser, in tests and in these docs, and the registry never learns your IPC names.
Keeping your copy in sync
The registry stays the source of a component after you install it. To pull a newer version, add it again with --overwrite:
$ pnpm dlx shadcn@latest add @videorc/button --overwriteThen run your formatter on the files it touched, so the diff shows only real changes, and review it. Don't edit a synced file in place: change it upstream, or stop syncing that item, as Videorc does with its own icon set.