This app uses a modern CSS stack optimized for Tauri desktop applications:
- Tailwind CSS v4 with CSS-based configuration
- shadcn/ui v4 component library
- OKLCH color space for perceptually uniform colors
- Desktop-specific defaults for native app feel
Tailwind v4 uses CSS-based configuration instead of tailwind.config.js.
src/
├── App.css # Main window styles + Tailwind imports
├── quick-pane.css # Quick pane window styles
└── theme-variables.css # Shared theme variables (colors, radii)
Multi-window theming: theme-variables.css is imported by both App.css and quick-pane.css so all windows share the same theme tokens. When adding new color variables, add them to theme-variables.css.
@import 'tailwindcss'; /* Core Tailwind */
@import 'tw-animate-css'; /* Animation utilities */
@custom-variant dark (&:is(.dark *)); /* Dark mode variant */
@theme inline {
/* Map CSS variables to Tailwind tokens */
--color-background: var(--background);
--color-foreground: var(--foreground);
/* ... */
}
:root {
/* Light mode values */
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
}
.dark {
/* Dark mode overrides */
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
}
@layer base {
/* Global base styles */
}| Directive | Purpose |
|---|---|
@theme inline |
Maps CSS variables to Tailwind's design token system |
@custom-variant dark |
Enables dark: prefix based on .dark class |
@layer base |
Base styles that apply globally |
To add a new semantic color:
@theme inline {
--color-success: var(--success);
--color-success-foreground: var(--success-foreground);
}
:root {
--success: oklch(0.7 0.15 145);
--success-foreground: oklch(1 0 0);
}
.dark {
--success: oklch(0.6 0.15 145);
--success-foreground: oklch(1 0 0);
}Then use with Tailwind: bg-success text-success-foreground
- ThemeProvider (
src/components/ThemeProvider.tsx) manages theme state - Adds
.darkclass to<html>element when dark mode is active - CSS variables in
.darkoverride:rootvalues - Tailwind's
dark:variant applies styles conditionally
light- Force light modedark- Force dark modesystem- Follow OS preference (default)
// Access theme in components
import { useTheme } from '@/hooks/use-theme'
function MyComponent() {
const { theme, setTheme } = useTheme()
return <button onClick={() => setTheme('dark')}>Current: {theme}</button>
}This app uses the .dark class approach rather than CSS light-dark() because:
- Standard pattern for shadcn/ui ecosystem
- JavaScript control over theme switching
- Supports "system" preference detection
- Compatible with all shadcn components
All colors use the OKLCH color space for perceptual uniformity.
oklch(lightness chroma hue)
oklch(0.7 0.15 250) /* L: 0-1, C: 0-0.4, H: 0-360 */- Perceptually uniform - Equal steps in values = equal perceived change
- Wide gamut - Access to P3 display colors
- Intuitive - Lightness is predictable (unlike HSL)
| Token | Purpose |
|---|---|
--background / --foreground |
Page background and text |
--card / --card-foreground |
Card surfaces |
--primary / --primary-foreground |
Primary actions |
--secondary / --secondary-foreground |
Secondary actions |
--muted / --muted-foreground |
Subdued elements |
--accent / --accent-foreground |
Highlights |
--destructive |
Destructive actions (red) |
--border / --input / --ring |
Borders and focus rings |
The @layer base section includes styles that make the app feel native on desktop.
body {
user-select: none; /* Disable by default */
}
input,
textarea,
[contenteditable='true'] {
user-select: text !important; /* Enable in editable areas */
}Why: Desktop apps typically don't allow selecting UI text, only content.
* {
cursor: default; /* Arrow cursor everywhere */
}
input,
textarea {
cursor: text !important;
}
.cursor-pointer {
cursor: pointer !important;
}Why: Native apps use arrow cursor, not text cursor on labels.
body {
overscroll-behavior: none; /* Prevent bounce/refresh */
overflow: hidden; /* Prevent body scroll */
}Why: Prevents pull-to-refresh and elastic scrolling that feels wrong in desktop apps.
*[data-tauri-drag-region] {
-webkit-app-region: drag;
app-region: drag;
}Apply data-tauri-drag-region to elements that should drag the window (like title bars).
src/components/
├── layout/ # App structure
│ ├── MainWindow.tsx
│ ├── LeftSideBar.tsx
│ ├── RightSideBar.tsx
│ └── MainWindowContent.tsx
├── titlebar/ # Window chrome
│ ├── TitleBar.tsx
│ ├── MacOSWindowControls.tsx
│ └── WindowsWindowControls.tsx
├── ui/ # shadcn primitives
│ ├── button.tsx
│ ├── dialog.tsx
│ └── ...
├── command-palette/ # Command palette feature
├── preferences/ # Preferences dialog
├── ThemeProvider.tsx
└── ErrorBoundary.tsx
- layout/ - Structural components that define app regions
- titlebar/ - Platform-specific window controls
- ui/ - shadcn/ui primitives (don't modify directly)
- Feature folders - Group related components together
npx shadcn@latest add button
npx shadcn@latest add dialogComponents are copied to src/components/ui/ and can be customized.
shadcn components are yours to modify. Common customizations:
// src/components/ui/button.tsx
const buttonVariants = cva('...', {
variants: {
variant: {
default: 'bg-primary text-primary-foreground',
// Add custom variant
success: 'bg-success text-success-foreground',
},
},
})This app includes commonly needed components. Run npx shadcn@latest add [component] to add more from ui.shadcn.com.
All components use the cn() utility for conditional classes:
import { cn } from '@/lib/utils'
function MyComponent({ className, disabled }) {
return (
<div
className={cn(
'base-styles here',
disabled && 'opacity-50',
className // Allow overrides
)}
>
...
</div>
)
}Pattern: Always accept className prop and merge with cn() for flexibility.
Layout components should:
- Accept
childrenandclassNameprops - Use flexbox with
overflow-hiddento prevent content bleed - Not set external margins (let parent control spacing)
interface SideBarProps {
children?: React.ReactNode
className?: string
}
export function LeftSideBar({ children, className }: SideBarProps) {
return (
<div className={cn('flex flex-col h-full overflow-hidden', className)}>
{children}
</div>
)
}For panels that toggle visibility, prefer CSS over conditional rendering:
// Good: Preserves component state
;<ResizablePanel className={cn(!visible && 'hidden')}>
<SideBar />
</ResizablePanel>
// Avoid: Loses component state on hide/show
{
visible && <SideBar />
}This preserves scroll position, form state, and resize dimensions.
- Use semantic color tokens (
bg-background,text-foreground) - Accept
classNameprop on components - Use
cn()for conditional classes - Keep desktop UX conventions (cursor, selection, scroll)
- Follow existing patterns in codebase
- Use raw color values (
bg-white,text-gray-900) - Hardcode light/dark specific values
- Override shadcn components in place (copy and modify instead)
- Add
cursor-pointereverywhere (only for actual clickable elements) - Use viewport-based responsive design (this is a fixed-size desktop app)