This guide covers how to consume SpaceUI packages from an external project (outside the SpaceUI monorepo). It documents the real-world integration pattern used by Spacedrive, including publishing, local development linking, Vite transpilation, and Tailwind v4 scanning.
- Installing from npm
- Local Development (Linking)
- Vite Configuration
- Tailwind CSS v4
- Using Components
- Publishing & Releases
- React Native / NativeWind
- Next.js
- Troubleshooting
- Bun 1.1.0+
- Vite (or another bundler — this guide focuses on Vite)
- Tailwind CSS v4
- React 18 or 19
SpaceUI is designed to live as a sibling repo alongside your project:
your-workspace/
├── spaceui/ # This repo
└── your-project/ # Your app
All relative paths in this guide assume this layout. Adjust if your setup differs.
SpaceUI packages are published publicly on npm under the @spacedrive scope. For production use, install published versions:
bun add @spacedrive/tokens @spacedrive/primitives
# Add whichever packages you need:
bun add @spacedrive/forms @spacedrive/ai @spacedrive/explorerThis gives you pre-built dist/ output — no source aliases or special Vite config needed beyond Tailwind setup (Step 3) and peer dependencies (Step 4).
For local development against an unreleased version of SpaceUI, see the next section.
When you're iterating on SpaceUI and a consuming project simultaneously, use Bun's linking to symlink local packages instead of pulling from npm.
cd spaceui
bun run linkThis runs bun link inside each package directory, registering @spacedrive/tokens, @spacedrive/primitives, @spacedrive/forms, @spacedrive/ai, and @spacedrive/explorer in Bun's global link registry.
cd your-project
bun link @spacedrive/tokens
bun link @spacedrive/primitives
# ... and whichever other packages you needWhile developing locally, override the npm versions with the link: protocol:
{
"dependencies": {
"@spacedrive/tokens": "link:@spacedrive/tokens",
"@spacedrive/primitives": "link:@spacedrive/primitives",
"@spacedrive/ai": "link:@spacedrive/ai",
"@spacedrive/forms": "link:@spacedrive/forms",
"@spacedrive/explorer": "link:@spacedrive/explorer"
}
}Note:
link:@spacedrive/primitivestells Bun to resolve via the global link registry, not a local file path. This is different fromlink:../spaceui/packages/primitives.
When you're done developing locally, switch back to versioned ranges for production:
{
"dependencies": {
"@spacedrive/tokens": "^0.1.0",
"@spacedrive/primitives": "^0.1.0"
}
}If you installed from npm and are NOT using
link:for local development, you can skip sections 2a-2c. You only need the React deduplication (2d) and the Tailwind setup (Step 3). Published packages resolve to pre-builtdist/output automatically.
This is where most of the complexity lives for local development. Vite needs four things configured to work with linked SpaceUI packages.
SpaceUI packages ship with dist/ output, but for local development you want Vite to resolve directly to source TypeScript files. This gives you instant HMR without needing to rebuild SpaceUI:
// vite.config.ts
import path from 'path';
export default defineConfig({
resolve: {
alias: [
// Tokens — CSS imports need the directory, not a JS entry
{
find: '@spacedrive/tokens/src/css',
replacement: path.resolve(__dirname, '../spaceui/packages/tokens/src/css')
},
{
find: '@spacedrive/tokens',
replacement: path.resolve(__dirname, '../spaceui/packages/tokens')
},
// Component packages — point to source index.ts
{
find: '@spacedrive/primitives',
replacement: path.resolve(__dirname, '../spaceui/packages/primitives/src/index.ts')
},
{
find: '@spacedrive/ai',
replacement: path.resolve(__dirname, '../spaceui/packages/ai/src/index.ts')
},
{
find: '@spacedrive/forms',
replacement: path.resolve(__dirname, '../spaceui/packages/forms/src/index.ts')
},
{
find: '@spacedrive/explorer',
replacement: path.resolve(__dirname, '../spaceui/packages/explorer/src/index.ts')
},
]
}
});Important: The
@spacedrive/tokens/src/cssalias must come before the@spacedrive/tokensalias. Vite matches aliases in order, and the more specific path needs to match first.
Vite pre-bundles dependencies for performance. SpaceUI packages must be excluded, otherwise Vite caches a snapshot and won't pick up changes:
export default defineConfig({
optimizeDeps: {
exclude: ['@spacedrive/tokens', '@spacedrive/primitives', '@spacedrive/ai', '@spacedrive/forms', '@spacedrive/explorer']
}
});Vite's dev server restricts file reads to the project root by default. Since SpaceUI lives outside your project, you must explicitly allow it:
export default defineConfig({
server: {
fs: {
allow: [
path.resolve(__dirname, '..'), // parent directory
path.resolve(__dirname, '../spaceui') // spaceui repo
]
}
}
});When two repos both depend on React, Vite can end up bundling two copies, which causes the infamous "Invalid hook call" error. Pin React to your project's copy:
export default defineConfig({
resolve: {
dedupe: ['react', 'react-dom'],
alias: [
{ find: /^react$/, replacement: path.resolve(__dirname, './node_modules/react/index.js') },
{ find: /^react\/jsx-runtime$/, replacement: path.resolve(__dirname, './node_modules/react/jsx-runtime.js') },
{ find: /^react\/jsx-dev-runtime$/, replacement: path.resolve(__dirname, './node_modules/react/jsx-dev-runtime.js') },
{ find: /^react-dom$/, replacement: path.resolve(__dirname, './node_modules/react-dom/index.js') },
{ find: /^react-dom\/client$/, replacement: path.resolve(__dirname, './node_modules/react-dom/client.js') },
// ... your @spacedrive aliases from 2a ...
]
}
});import path from 'path';
import tailwindcss from '@tailwindcss/vite';
import react from '@vitejs/plugin-react-swc';
import { defineConfig } from 'vite';
const spaceui = path.resolve(__dirname, '../spaceui/packages');
export default defineConfig({
plugins: [react(), tailwindcss()],
resolve: {
dedupe: ['react', 'react-dom'],
alias: [
// Pin React to a single copy
{ find: /^react$/, replacement: path.resolve(__dirname, './node_modules/react/index.js') },
{ find: /^react\/jsx-runtime$/, replacement: path.resolve(__dirname, './node_modules/react/jsx-runtime.js') },
{ find: /^react\/jsx-dev-runtime$/, replacement: path.resolve(__dirname, './node_modules/react/jsx-dev-runtime.js') },
{ find: /^react-dom$/, replacement: path.resolve(__dirname, './node_modules/react-dom/index.js') },
{ find: /^react-dom\/client$/, replacement: path.resolve(__dirname, './node_modules/react-dom/client.js') },
// SpaceUI — resolve to source for HMR
{ find: '@spacedrive/tokens/src/css', replacement: `${spaceui}/tokens/src/css` },
{ find: '@spacedrive/tokens', replacement: `${spaceui}/tokens` },
{ find: '@spacedrive/primitives', replacement: `${spaceui}/primitives/src/index.ts` },
{ find: '@spacedrive/ai', replacement: `${spaceui}/ai/src/index.ts` },
{ find: '@spacedrive/forms', replacement: `${spaceui}/forms/src/index.ts` },
{ find: '@spacedrive/explorer', replacement: `${spaceui}/explorer/src/index.ts` },
]
},
optimizeDeps: {
exclude: ['@spacedrive/tokens', '@spacedrive/primitives', '@spacedrive/ai', '@spacedrive/forms', '@spacedrive/explorer']
},
server: {
fs: {
allow: [
path.resolve(__dirname, '..'),
path.resolve(__dirname, '../spaceui')
]
}
}
});In your app's main CSS entry point:
@import "tailwindcss";
/* SpaceUI design tokens — must come after tailwindcss import */
@import "@spacedrive/tokens/src/css/theme.css";
@import "@spacedrive/tokens/src/css/base.css";
/* Include whichever themes you need */
@import "@spacedrive/tokens/src/css/themes/light.css";
@import "@spacedrive/tokens/src/css/themes/dark.css";
@import "@spacedrive/tokens/src/css/themes/midnight.css";
@import "@spacedrive/tokens/src/css/themes/noir.css";
@import "@spacedrive/tokens/src/css/themes/slate.css";
@import "@spacedrive/tokens/src/css/themes/nord.css";
@import "@spacedrive/tokens/src/css/themes/mocha.css";SpaceUI components use Tailwind utility classes internally. Tailwind v4 needs @source directives so it knows to scan those files and include the right classes in your build:
/* Adjust relative paths based on your project structure */
@source "../../spaceui/packages/primitives/src";
@source "../../spaceui/packages/ai/src";
@source "../../spaceui/packages/explorer/src";
@source "../../spaceui/packages/forms/src";Why
@sourceinstead ofcontent? Tailwind v4 uses CSS-native@sourcedirectives instead of thecontentarray from v3's JS config. These paths are resolved relative to the CSS file they appear in.
Once the theme is imported, you get these Tailwind classes:
| Category | Classes |
|---|---|
| Accent | bg-accent, text-accent, border-accent, bg-accent-faint, bg-accent-deep |
| Text | text-ink, text-ink-dull, text-ink-faint |
| App | bg-app, bg-app-box, bg-app-hover, bg-app-selected, border-app-line, bg-app-input |
| Sidebar | bg-sidebar, bg-sidebar-box, text-sidebar-ink, border-sidebar-line, bg-sidebar-selected |
| Menu | bg-menu, text-menu-ink, bg-menu-hover, bg-menu-selected, border-menu-line |
| Status | text-status-success, text-status-warning, text-status-error, text-status-info |
All colors support opacity modifiers: bg-accent/50, bg-sidebar/65, etc.
// Primitives
import { Button, Input, Dialog, DropdownMenu, Badge, Card, Tooltip } from '@spacedrive/primitives';
// Forms (requires react-hook-form + zod as peer deps)
import { Form, InputField, SelectField, CheckboxField } from '@spacedrive/forms';
// AI components (requires @tanstack/react-query + @tanstack/react-virtual as peer deps)
import { ToolCall, Markdown, MessageBubble, ModelSelector, TaskRow } from '@spacedrive/ai';
// Explorer
import { TagPill, RenameInput } from '@spacedrive/explorer';Each package declares its peer dependencies. Install them in your project:
# For @spacedrive/primitives
bun add react react-dom tailwindcss
# For @spacedrive/forms (in addition to the above)
bun add react-hook-form zod
# For @spacedrive/ai (in addition to primitives peers)
bun add @tanstack/react-query @tanstack/react-virtualRun SpaceUI in watch mode alongside your app for live rebuilds:
# Terminal 1 — SpaceUI (only needed if NOT using source aliases)
cd spaceui
bun run dev
# Terminal 2 — Your app
cd your-project
bun run devIf you set up Vite source aliases (Step 2a), you don't need SpaceUI's watch mode at all. Vite transpiles SpaceUI source files directly on demand and picks up changes via HMR.
SpaceUI packages are published publicly to npm under the @spacedrive scope. Releases are managed with Changesets and automated via GitHub Actions.
- Linked packages:
@spacedrive/primitives,@spacedrive/forms,@spacedrive/ai, and@spacedrive/explorerare version-linked — they always release together at the same version. A breaking change in primitives bumps all four. - Independent packages:
@spacedrive/tokensis versioned independently, since design token updates don't necessarily require component changes. - Internal dependencies: When a linked package bumps, any internal
@spacedrive/*dependency is automatically updated to the new version (as a patch bump).
When your PR includes user-facing changes, create a changeset describing what changed:
bun run changesetThis walks you through an interactive prompt:
- Select which packages were affected
- Choose the semver bump type (patch / minor / major)
- Write a summary of the change
It creates a markdown file in .changeset/ — commit this with your PR.
CI runs on every PR (typecheck, build, export verification). The changeset job warns if no changeset is included.
When changesets land on main, the release workflow automatically opens (or updates) a "Version Packages" PR. This PR:
- Bumps version numbers in all affected
package.jsonfiles - Updates
CHANGELOG.mdin each package - Updates internal
@spacedrive/*dependency ranges
Review this PR to see exactly what versions will be published.
When you merge the "Version Packages" PR, the release workflow runs again and:
- Builds all packages (
turbo run build) - Publishes to npm (
changeset publish) - Creates git tags for each released version
The GitHub Actions workflow (.github/workflows/release.yml) needs one secret:
- Generate an npm access token:
npm token create(or create one at npmjs.com → Access Tokens) - Add it as
NPM_TOKENin your GitHub repo: Settings → Secrets and variables → Actions → New repository secret
The GITHUB_TOKEN is provided automatically by GitHub Actions.
Once packages are published, consuming projects should use versioned ranges instead of link::
{
"dependencies": {
"@spacedrive/tokens": "^0.1.0",
"@spacedrive/primitives": "^0.1.0",
"@spacedrive/ai": "^0.1.0",
"@spacedrive/forms": "^0.1.0",
"@spacedrive/explorer": "^0.1.0"
}
}With published packages, you can remove the Vite source aliases (Step 2a), optimizeDeps.exclude (Step 2b), and server.fs.allow (Step 2c). Imports will resolve to the pre-built dist/ output in node_modules. You still need:
- React deduplication (Step 2d)
- Tailwind CSS setup (Step 3) —
@sourcepaths change to point intonode_modules:
@source "node_modules/@spacedrive/primitives/dist";
@source "node_modules/@spacedrive/ai/dist";
@source "node_modules/@spacedrive/explorer/dist";
@source "node_modules/@spacedrive/forms/dist";If you need to publish without the GitHub Action:
cd spaceui
# 1. Create changeset (if not already done)
bun run changeset
# 2. Bump versions
bun run version-packages
# 3. Review changes, then commit
git add .
git commit -m "chore: version packages"
# 4. Build and publish
bun run publish
# 5. Push tags
git push --follow-tagsFor testing before a stable release:
# Enter pre-release mode
bunx changeset pre enter alpha
# Create changesets and version as normal
bun run changeset
bun run version-packages
# This produces versions like 0.1.0-alpha.0
# Publish pre-release
bun run publish --tag alpha
# Exit pre-release mode when ready
bunx changeset pre exitConsumers install pre-releases explicitly: bun add @spacedrive/primitives@alpha
For React Native projects using NativeWind, you can't import CSS files directly. SpaceUI provides raw-colors.cjs — a CommonJS export of raw HSL values:
// tailwind.config.js (React Native)
const sharedColors = require('@spacedrive/tokens/raw-colors');
module.exports = {
theme: {
extend: {
colors: {
accent: sharedColors.accent,
ink: sharedColors.ink,
sidebar: sharedColors.sidebar,
app: sharedColors.app,
menu: sharedColors.menu,
}
}
}
};For Next.js projects, the Vite-specific config doesn't apply. Instead:
Tell Next.js to transpile SpaceUI packages (they ship as ESM with JSX):
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
transpilePackages: [
'@spacedrive/primitives',
'@spacedrive/ai',
'@spacedrive/forms',
'@spacedrive/explorer',
],
};
export default nextConfig;The Tailwind v4 CSS setup (Step 3) is the same — add @import and @source directives in your global CSS file.
Next.js handles React deduplication automatically. No extra config needed.
React is being bundled twice. Make sure you have the React aliases from Step 2d, and that dedupe: ['react', 'react-dom'] is set.
Your CSS file is missing the @source directives from Step 3. Tailwind v4 only scans paths it's told about.
The @spacedrive/tokens/src/css alias must be listed before the @spacedrive/tokens alias. Vite matches the first alias that fits.
- Check that
optimizeDeps.excludeincludes all@spacedrive/*packages - Try deleting
node_modules/.viteto clear Vite's cache - If using dist (not source aliases), make sure
bun run devis running in SpaceUI
Add the SpaceUI directory to server.fs.allow (Step 2c).
# Re-register in spaceui
cd spaceui && bun run link
# Re-link in your project
cd your-project && bun link @spacedrive/primitives
# Verify it's linked
ls -la node_modules/@spacedrive/primitives
# Should show a symlinkRunning bun install can remove links. Re-run bun link @spacedrive/primitives (etc.) after installing new dependencies. The link: protocol in package.json should prevent this, but if it doesn't, re-link.
When you're done with local development and want to switch back to published versions:
# In spaceui
bun run unlink
# In your project — switch back to npm versions
# Update package.json: "link:@spacedrive/primitives" → "^0.1.0"
bun install| Task | Command |
|---|---|
| Install from npm | bun add @spacedrive/primitives @spacedrive/tokens |
| Register for linking | cd spaceui && bun run link |
| Link into your project | cd your-project && bun link @spacedrive/primitives |
| Watch mode | cd spaceui && bun run dev |
| Build all packages | cd spaceui && bun run build |
| Create a changeset | cd spaceui && bun run changeset |
| Bump versions | cd spaceui && bun run version-packages |
| Build + publish to npm | cd spaceui && bun run publish |
| Unlink | cd spaceui && bun run unlink |
| Clear Vite cache | rm -rf node_modules/.vite |