Using Bamboo in a Component Library
Publish compiled components and CSS, or share source and presets with a Vite application.
Choose the publishing model based on who owns the styles:
| You want to ship | Build and consumption model |
|---|---|
| Tokens, patterns, or recipes | Publish a preset for the app's Bamboo config |
| Components with their own compiled styles | Build JavaScript and CSS with Vite |
| Components styled by the consuming app's theme | Share source for the app's Vite compiler |
Every component that executes a Bamboo style call must pass through @bamboocss/vite. Running tsdown and then
bamboo cssgen generates JavaScript and CSS separately, but leaves the JavaScript's style calls uncompiled. Those calls
throw when a consumer renders the component.
Ship a Bamboo Preset
A preset contains configuration rather than components. It can be built with a regular TypeScript bundler.
src/index.ts
import { definePreset } from '@bamboocss/dev'
export const acmePreset = definePreset({
theme: {
extend: {
tokens: {
colors: { primary: { value: '#2563eb' } },
},
},
},
})pnpm tsdown src/index.ts
The consuming app includes the default presets alongside yours:
bamboo.config.ts
import { acmePreset } from '@acme-org/bamboo-preset'
import { defineConfig } from '@bamboocss/dev'
import { defaultPresets } from '@bamboocss/dev/presets'
export default defineConfig({
presets: [...defaultPresets, acmePreset],
include: ['./src/**/*.{ts,tsx,js,jsx}'],
outdir: 'styled-system',
})presets is the complete list. Omitting the defaults also omits their utilities, patterns, conditions, and tokens. The
app still needs the Vite integration.
Ship a Static CSS File
Build the library's JavaScript and stylesheet together with Vite. Consumers import the compiled components and CSS; they do not need Bamboo to execute those components.
Configure the library
Install the build dependencies and React, then initialize Bamboo:
pnpm add -D @bamboocss/dev @bamboocss/vite vite typescript @types/react
pnpm add react
pnpm bamboo init
bamboo.config.ts
import { defineConfig } from '@bamboocss/dev'
export default defineConfig({
include: ['./src/**/*.{ts,tsx}'],
outdir: 'styled-system',
prefix: 'acme',
})The prefix separates this library's classes and variables from a consuming app's Bamboo output.
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx",
"types": ["vite/client", "@bamboocss/vite/client"],
"strict": true
},
"include": ["src"]
}vite.config.ts
import bamboocss from '@bamboocss/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [bamboocss({ pruneCss: false })],
build: {
lib: {
entry: 'src/index.tsx',
formats: ['es'],
fileName: 'index',
cssFileName: 'styles',
},
rollupOptions: {
external: ['react', 'react/jsx-runtime'],
},
},
})pruneCss: false keeps the library's extracted utility sheet and its stable styles.css filename. Vite library mode
uses one stylesheet by default. Declare React as a peer dependency in the published package.
Author and build the components
Set "type": "module" in the library's package.json before building so the ESM entry uses the .js extension.
src/index.tsx
import type { ReactNode } from 'react'
import { css } from '../styled-system/css'
import 'virtual:bamboo.css'
export function Button({ children }: { children: ReactNode }) {
return (
<button type="button" className={css({ bg: 'red.300', px: '2', py: '3' })}>
{children}
</button>
)
}pnpm vite build
For an ESM package ("type": "module" in package.json), this writes dist/index.js and dist/styles.css. The
stylesheet includes its own layer order. Export both files and keep CSS imports from being dropped as side effects:
package.json
{
"name": "@acme-org/design-system",
"type": "module",
"files": ["dist"],
"exports": {
".": "./dist/index.js",
"./styles.css": "./dist/styles.css"
},
"sideEffects": ["**/*.css"],
"peerDependencies": {
"react": ">=19"
}
}This is the runtime export setup. If you publish TypeScript declarations, generate them separately and add a types
entry for the component export; Vite's JavaScript build does not emit declarations.
Consume the library
src/App.tsx
import { Button } from '@acme-org/design-system'
import '@acme-org/design-system/styles.css'
export function App() {
return <Button>Click me</Button>
}The library's theme and class names were resolved when it was built. A consumer's Bamboo config does not regenerate them. Expose finite recipe variants for supported component choices, or publish a preset and source when the consuming app needs to control the theme.
Use Bamboo as external package
When the app compiles shared component source, the library's imports identify an authoring surface owned by the app. Keep those imports available to the app's compiler instead of bundling a generated styling runtime into the library.
Include the src files
For example, a workspace can contain packages/app and packages/components. The component package exports source:
packages/components/src/index.tsx
import { css } from '@acme-org/styled-system/css'
export function Button() {
return (
<button type="button" className={css({ color: 'primary' })}>
Save
</button>
)
}Configure Bamboo in packages/app, including both source trees and the preset that supplies primary:
packages/app/bamboo.config.ts
import { acmePreset } from '@acme-org/bamboo-preset'
import { defineConfig } from '@bamboocss/dev'
import { defaultPresets } from '@bamboocss/dev/presets'
export default defineConfig({
presets: [...defaultPresets, acmePreset],
include: ['./src/**/*.{ts,tsx}', '../components/src/**/*.{ts,tsx}'],
importMap: '@acme-org/styled-system',
outdir: 'styled-system',
})Resolve the component import to source and the authoring import to the app's generated directory:
packages/app/vite.config.ts
import bamboocss from '@bamboocss/vite'
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [bamboocss()],
resolve: {
alias: {
'@acme-org/components': fileURLToPath(new URL('../components/src/index.tsx', import.meta.url)),
'@acme-org/styled-system': fileURLToPath(new URL('./styled-system', import.meta.url)),
},
},
})The app imports Button from @acme-org/components and imports virtual:bamboo.css once in its entry module. Its Vite
compiler sees the component source, and its Bamboo config controls token values. Add matching TypeScript paths if your
editor needs to resolve these aliases; Vite aliases alone configure the bundler.
For a published source package, resolve its exports to those source files, include them in Bamboo's source inventory,
and ensure Vite transforms them rather than treating them as a prebundled or external dependency. During SSR that can
require Vite's ssr.noExternal for the component package. Merely adding source to Bamboo's include does not make an
external dependency pass through Vite's compiler.
Ship the build info file
bamboo ship --outfile dist/bamboo.buildinfo.json serializes extraction results. It does not compile a component's
JavaScript, and a build-info file cannot replace the source modules the Vite compiler must transform. Do not use it to
ship uncompiled components while hiding their source. Use the compiled library approach when
consumers should receive only your build output.
FAQ
How to override tokens used by the @acme-org/styled-system package?
With shared source, extend the token in the consuming app's config:
bamboo.config.ts
import { defineConfig } from '@bamboocss/dev'
export default defineConfig({
// Keep the presets, include, importMap, and outdir from the source-sharing setup.
theme: {
extend: {
tokens: {
colors: { primary: { value: '#dc2626' } },
},
},
},
})With a precompiled library, use that library's documented theming API; the app's config does not recompile its styles.
Troubleshooting
An error containing was not compiled means a style call survived the build. Check that bamboocss() transforms the
actual module the consumer imports, its source is in Bamboo's include, and the build imports virtual:bamboo.css.
Changing outExtension or adding a CSS file alone does not compile the JavaScript.