Installation
Installation
Section titled “Installation”Basalt UI is a Tailwind CSS v4 design system package. Follow these steps to install and configure it.
Quick Start
Section titled “Quick Start”Step 1: Install Basalt UI and Dependencies
Section titled “Step 1: Install Basalt UI and Dependencies”# Using Bun (recommended)bun add basalt-uibun add -D @tailwindcss/typography shadcn tw-animate-css
# Using npmnpm install basalt-uinpm install -D @tailwindcss/typography shadcn tw-animate-css
# Using pnpmpnpm add basalt-uipnpm add -D @tailwindcss/typography shadcn tw-animate-cssWhy these dependencies?
@tailwindcss/typography- Powers the.proseclass for content stylingshadcn- Component CLI tool for adding UI componentstw-animate-css- Animation utilities
Note: These are peer dependencies, meaning you install them separately. This prevents version conflicts and lets you control which versions you use.
Step 2: Import the CSS
Section titled “Step 2: Import the CSS”Add this to your main CSS file:
/* src/styles/globals.css or app/globals.css */@import "basalt-ui/css";⚠️ Critical: This import REPLACES @import "tailwindcss" - do NOT use both!
/* ❌ WRONG - Don't import both */@import "tailwindcss";@import "basalt-ui/css";
/* ✅ CORRECT - Only basalt-ui */@import "basalt-ui/css";Why? BasaltUI includes the complete Tailwind v4 theme. Importing both causes conflicts and duplicate CSS.
Import path formats:
- ✅
basalt-ui/css(correct) - ❌
basalt-ui(wrong) - ❌
basalt-ui/src/index.css(wrong)
What this imports:
- Complete Tailwind v4 theme (
@theme inlineCSS) - Self-hosted variable fonts (Instrument Sans Variable, JetBrains Mono Variable) with
font-display: swap - CSS variables for light/dark modes
- Typography plugin configuration
- Animation utilities
What you DON’T need:
- No
tailwind.config.jsfile required - No manual font setup
- No PostCSS configuration
Tailwind version requirement: Basalt UI requires Tailwind CSS v4.1.18 or newer. Earlier v4.0.x versions had critical bugs that are fixed in v4.1.18+.
Step 3: Add Dark Mode
Section titled “Step 3: Add Dark Mode”Add the dark class to your HTML element:
<html class="dark"> <!-- Your app --></html>Dynamic theme switching:
See the Dark Mode Toggle section below for complete React/Vue component examples.
Step 4: Custom CSS Variables (Optional)
Section titled “Step 4: Custom CSS Variables (Optional)”If you need project-specific design tokens or overrides, add them AFTER the basalt-ui import:
@import "basalt-ui/css"; /* Must come first */
/* Your custom overrides come after */:root { --my-custom-color: oklch(0.7 0.1 200); --my-spacing: 2rem;}
/* Override BasaltUI tokens if needed (not recommended) */.dark { --background: oklch(0.15 0.01 285); /* Darker than default */}Important ordering rules:
- ✅
@import "basalt-ui/css"must be first - ✅ Your custom CSS variables come after
- ❌ Don’t define custom variables before the import (they’ll be overridden)
Framework-Specific Setup
Section titled “Framework-Specific Setup”Vite (React, Vue, Svelte)
Section titled “Vite (React, Vue, Svelte)”1. Install Tailwind v4 Vite plugin:
bun add -D @tailwindcss/vite2. Configure Vite:
import { defineConfig } from 'vite';import react from '@vitejs/plugin-react';import tailwindcss from '@tailwindcss/vite';
export default defineConfig({ plugins: [ tailwindcss(), // ⚠️ Must come before framework plugin react(), ],});3. Import CSS in entry file:
import './styles/globals.css';4. Create globals.css:
@import "basalt-ui/css";Next.js (App Router)
Section titled “Next.js (App Router)”1. Create global CSS file:
@import "basalt-ui/css";2. Import in root layout:
import './globals.css';
export default function RootLayout({ children }) { return ( <html className="dark"> <body>{children}</body> </html> );}Note: Tailwind v4 CSS imports work automatically in Next.js 15+ with App Router. No config file needed!
1. Install Tailwind CSS v4 Vite plugin:
bun add -D @tailwindcss/vite2. Add plugin to Astro config:
import { defineConfig } from 'astro/config';import tailwindcss from '@tailwindcss/vite';
export default defineConfig({ vite: { plugins: [tailwindcss()], },});3. Create global CSS file:
@import "basalt-ui/css";4. Import in layout:
---import '../styles/global.css';---<html class="dark"> <body> <slot /> </body></html>Font Loading
Section titled “Font Loading”Basalt UI includes self-hosted variable fonts by default (Instrument Sans Variable and JetBrains Mono Variable). No additional setup needed.
@import "basalt-ui/css"; /* Includes fonts, no separate import needed */Fonts use font-display: swap — fallback text renders immediately, the web font swaps in when loaded. This avoids invisible text and benefits Core Web Vitals.
Why self-hosted?
- Privacy: No external CDN requests
- Performance: Fonts served from your domain (faster, no DNS lookup)
- Reliability: No dependency on Google Fonts availability
- Control: Font files bundled with your app
Opinionated by design:
Basalt UI uses Instrument Sans Variable and JetBrains Mono Variable as non-negotiable defaults. This ensures consistency across all integrations (ShadCN, Starlight, Tremor) and eliminates decision fatigue.
If you need different fonts: Fork the package and modify the font imports in src/index.css, or override the CSS variables (breaks consistency with Basalt UI philosophy).
Integration with UI Libraries
Section titled “Integration with UI Libraries”ShadCN UI
Section titled “ShadCN UI”Basalt UI works seamlessly with ShadCN:
# Install ShadCN CLInpx shadcn@latest init
# Choose "Zinc" as base color when prompted
# Add componentsnpx shadcn@latest add buttonnpx shadcn@latest add cardnpx shadcn@latest add dialogHow it works:
- ShadCN components use classes like
bg-primary,text-foreground - Basalt UI provides these via CSS variables
- Dark mode switches automatically with
.darkclass - No additional configuration needed
Tremor Charts
Section titled “Tremor Charts”Tremor Raw chart components use Basalt colors automatically:
import { AreaChart } from '@tremor/react';
<AreaChart data={chartData} index="date" categories={["sales"]} colors={["blue"]} // Uses Basalt OKLCH blue/>Available colors:
blue,red,emerald,amber,violet,cyan,indigo- Sequential:
chart-blue-1throughchart-blue-8
Starlight Documentation
Section titled “Starlight Documentation”For Astro Starlight, import the dedicated CSS file:
import starlight from '@astrojs/starlight';
export default defineConfig({ integrations: [ starlight({ title: 'My Docs', customCss: [ 'basalt-ui/starlight', ], }), ],});Self-Hosted Variable Fonts
Section titled “Self-Hosted Variable Fonts”Basalt UI includes two self-hosted variable fonts:
Instrument Sans Variable - Headings and body text
- Full variable font with weight axis (400-700)
- Optimized for UI and content
- Clean, modern sans-serif
JetBrains Mono Variable - Code blocks and monospace
- Full variable font with weight axis (400)
- Designed for programming
- Excellent readability
Font stack:
--font-heading: "Instrument Sans Variable", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-body: "Instrument Sans Variable", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;--font-mono: "JetBrains Mono Variable", "Menlo", "Monaco", "Courier New", monospace;Font Performance Optimization
Section titled “Font Performance Optimization”Basalt UI ships with font-display: swap by default — text is visible immediately using a system fallback font, then the web font swaps in when loaded. This avoids invisible text and prevents slow Speed Index scores.
The remaining challenge with swap is Cumulative Layout Shift (CLS): because system fonts have slightly different metrics (x-height, ascent, line spacing) than your web fonts, swapping can cause text to reflow and push content around.
The solution is metric-calibrated fallbacks — @font-face declarations for your system fonts that override their metrics to match the web font exactly. When metrics match, the swap is visually invisible and CLS reaches zero.
Recommended: fontaine Plugin
Section titled “Recommended: fontaine Plugin”fontaine automates fallback metric calculation. It reads your web font files at build time and generates the calibrated @font-face overrides for you.
Optimal fallback fonts by platform:
| Font | Mac | Windows | Linux / Android |
|---|---|---|---|
| Instrument Sans Variable | Helvetica Neue | Segoe UI | Roboto |
| JetBrains Mono Variable | Menlo / SF Mono | Consolas | Liberation Mono |
Install:
bun add fontaine# or: npm install fontaineAstro (astro.config.mjs):
import { FontaineTransform } from 'fontaine'
export default defineConfig({ vite: { plugins: [ tailwindcss(), FontaineTransform.vite({ fallbacks: { 'Instrument Sans Variable': ['Helvetica Neue', 'Segoe UI', 'Roboto', 'Arial'], 'JetBrains Mono Variable': ['Consolas', 'Menlo', 'SF Mono', 'Courier New'], }, // Required: @fontsource-variable URLs are transformed by Vite, // fontaine cannot resolve them without an explicit node_modules path. resolvePath: (id) => new URL('node_modules/' + id, import.meta.url), }), ], },})Vite (vite.config.ts):
import { FontaineTransform } from 'fontaine'
export default defineConfig({ plugins: [ FontaineTransform.vite({ fallbacks: { 'Instrument Sans Variable': ['Helvetica Neue', 'Segoe UI', 'Roboto', 'Arial'], 'JetBrains Mono Variable': ['Consolas', 'Menlo', 'SF Mono', 'Courier New'], }, resolvePath: (id) => new URL('node_modules/' + id, import.meta.url), }), react(), ],})Next.js (next.config.ts):
import { FontaineTransform } from 'fontaine'
const nextConfig = { webpack(config) { config.plugins.push( FontaineTransform.webpack({ fallbacks: { 'Instrument Sans Variable': ['Helvetica Neue', 'Segoe UI', 'Roboto', 'Arial'], 'JetBrains Mono Variable': ['Consolas', 'Menlo', 'SF Mono', 'Courier New'], }, resolvePath: (id) => new URL('node_modules/' + id, import.meta.url), }) ) return config },}Additional: Font Preloading
Section titled “Additional: Font Preloading”Preloading the two critical latin variable fonts makes them arrive before the CSS is even parsed, shrinking the fallback window significantly. This is complementary to fontaine — preload reduces how long the fallback shows, fontaine eliminates the visual jump when it swaps.
Astro layout (Layout.astro):
<head> <!-- Preload the two critical latin variable fonts --> <link rel="preload" as="font" type="font/woff2" crossorigin="anonymous" href="/node_modules/@fontsource-variable/instrument-sans/files/instrument-sans-latin-wdth-normal.woff2" /> <link rel="preload" as="font" type="font/woff2" crossorigin="anonymous" href="/node_modules/@fontsource-variable/jetbrains-mono/files/jetbrains-mono-latin-wght-normal.woff2" /></head>HTML / generic:
<link rel="preload" as="font" type="font/woff2" crossorigin="anonymous" href="/path/to/instrument-sans-latin-wdth-normal.woff2"/>Requirements
Section titled “Requirements”Peer Dependencies
Section titled “Peer Dependencies”- Tailwind CSS v4+ - Core CSS framework
- @tailwindcss/typography - Typography plugin for
.proseclass - tw-animate-css - Animation utilities
- shadcn - Component utilities
All must be installed in your project (listed above).
Browser Support
Section titled “Browser Support”Modern browsers with OKLCH color support:
- Chrome 111+
- Edge 111+
- Safari 16.4+
- Firefox 113+
Troubleshooting
Section titled “Troubleshooting”Error: ”./css” is not exported under the condition “style”
Section titled “Error: ”./css” is not exported under the condition “style””Cause: Using Tailwind v4 Vite plugin with older basalt-ui version.
Fix: Update basalt-ui to latest version:
bun update basalt-ui# ornpm update basalt-uiError: Can’t resolve ‘@tailwindcss/typography’
Section titled “Error: Can’t resolve ‘@tailwindcss/typography’”Cause: Peer dependencies not installed.
Fix: Install all required peer dependencies:
bun add -D @tailwindcss/typography shadcn tw-animate-cssError: Cannot find module ‘basalt-ui’
Section titled “Error: Cannot find module ‘basalt-ui’”Cause: Using incorrect import path.
Fix: Use basalt-ui/css (not basalt-ui or basalt-ui/src/index.css):
/* ❌ Wrong */@import "basalt-ui";@import "basalt-ui/src/index.css";
/* ✅ Correct */@import "basalt-ui/css";Styles not applying / Components look unstyled
Section titled “Styles not applying / Components look unstyled”Possible causes:
- CSS import order incorrect
- Missing Tailwind plugin configuration
- Dark mode class not applied
Fixes:
- Ensure
basalt-ui/cssis imported in your entry CSS file - For Vite: Add
@tailwindcss/viteplugin BEFORE framework plugin - For Next.js: Ensure globals.css is imported in root layout
- Add
class="dark"to your<html>element - Check browser console for CSS loading errors
Vite plugin order
Section titled “Vite plugin order”Error: Styles not loading in Vite projects
Cause: Tailwind plugin must come before framework plugin
Fix:
export default defineConfig({ plugins: [ tailwindcss(), // ✅ First react(), // ✅ Second ],});
// ❌ Wrong order:// plugins: [react(), tailwindcss()]Fonts not loading
Section titled “Fonts not loading”Symptom: Using fallback system fonts instead of Instrument Sans
Check:
- Verify
basalt-ui/cssis imported (fonts are included) - Check browser DevTools Network tab for font requests
- Look for
InstrumentSans-Variable.woff2andJetBrainsMono-Variable.woff2 - Ensure no Content Security Policy blocking fonts
Note: Fonts are bundled automatically - no additional setup needed!
ShadCN components wrong colors
Section titled “ShadCN components wrong colors”Symptom: ShadCN components not using Basalt colors
Fix:
- Import
basalt-ui/cssBEFORE runningshadcn init - Choose “Zinc” as base color during initialization
- Basalt provides CSS variables (
--background,--primary, etc.) - Components automatically use these
Verify:
<Button variant="default">Primary Button</Button>// Should be blue (Basalt primary color)Dark mode not working
Section titled “Dark mode not working”Symptom: Dark mode not activating
Cause: Missing .dark class on <html> element
Fix:
<!-- ✅ Correct --><html class="dark">
<!-- ❌ Wrong --><body class="dark"> <!-- Must be on html! -->Dark Mode Toggle Component
Section titled “Dark Mode Toggle Component”React Example
Section titled “React Example”'use client'; // For Next.js App Router
import { useEffect, useState } from 'react';
export function ThemeToggle() { const [theme, setTheme] = useState<'light' | 'dark'>('dark');
// Load theme from localStorage on mount useEffect(() => { const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null; const initialTheme = savedTheme || 'dark'; setTheme(initialTheme); document.documentElement.classList.toggle('dark', initialTheme === 'dark'); }, []);
const toggleTheme = () => { const newTheme = theme === 'dark' ? 'light' : 'dark'; setTheme(newTheme); document.documentElement.classList.toggle('dark', newTheme === 'dark'); localStorage.setItem('theme', newTheme); };
return ( <button onClick={toggleTheme} className="rounded-md bg-primary px-4 py-2 text-primary-foreground" aria-label="Toggle theme" > {theme === 'dark' ? '🌙 Dark' : '☀️ Light'} </button> );}Vue 3 Example
Section titled “Vue 3 Example”<script setup lang="ts">import { ref, onMounted } from 'vue';
const theme = ref<'light' | 'dark'>('dark');
onMounted(() => { const savedTheme = localStorage.getItem('theme') as 'light' | 'dark' | null; const initialTheme = savedTheme || 'dark'; theme.value = initialTheme; document.documentElement.classList.toggle('dark', initialTheme === 'dark');});
const toggleTheme = () => { const newTheme = theme.value === 'dark' ? 'light' : 'dark'; theme.value = newTheme; document.documentElement.classList.toggle('dark', newTheme === 'dark'); localStorage.setItem('theme', newTheme);};</script>
<template> <button @click="toggleTheme" class="rounded-md bg-primary px-4 py-2 text-primary-foreground" aria-label="Toggle theme" > {{ theme === 'dark' ? '🌙 Dark' : '☀️ Light' }} </button></template>Vanilla JavaScript
Section titled “Vanilla JavaScript”// Toggle dark modefunction toggleTheme() { const isDark = document.documentElement.classList.toggle('dark'); localStorage.setItem('theme', isDark ? 'dark' : 'light');}
// Load saved theme on page loadconst savedTheme = localStorage.getItem('theme') || 'dark';document.documentElement.classList.toggle('dark', savedTheme === 'dark');TypeScript errors
Section titled “TypeScript errors”If you see TypeScript errors related to basalt-ui, ensure you have the latest version:
bun update basalt-uiIf errors persist:
{ "compilerOptions": { "types": ["vite/client"] }}Monorepo resolution issues
Section titled “Monorepo resolution issues”Symptom: Can’t resolve basalt-ui/css in monorepo workspace
Fix (Option 1): Use relative path:
@import "../../../../packages/basalt-ui/src/index.css";Fix (Option 2): Use @source directive (Tailwind v4):
@import "tailwindcss";@import "basalt-ui/css";
@source "../../../packages/basalt-ui/src";Fix (Option 3): Ensure workspace protocol:
{ "dependencies": { "basalt-ui": "workspace:*" }}Next Steps
Section titled “Next Steps”After installation:
- Explore Typography for content styling
- Review Color System for theming
- Check Spacing for layout utilities