desert-ui
v0.1.4
Published
A themeable Svelte 5 component library — 42 components, 18 themes, zero hardcoded colors.
Maintainers
Readme
desert-ui
A themeable Svelte 5 component library — 42 components, 18 themes, zero hardcoded colors, runtime theme-switching.

Highlights
- 42 components — primitives, feedback, data display, layout, overlays, forms
- 18 built-in themes — 11 dark + 7 light, from warm stone to Primer navy
- Runtime theming — swap an entire theme with a single prop; no re-render flicker
- Responsive-first — sidebars become drawers, modals become bottom sheets at
≤1024px - Svelte 5 runes —
$state,$derived,$props, snippets,{@render} - Tailwind 4 peer dependency — no config file; CSS-first
- Ships typed — every component's props, variants, and option types exported
Screenshots
Default dark — component showcase

Figmint — light theme

Source theme — Table with status badges

Install
npm install desert-uiPeer dependencies (you must have these installed in your app):
npm install svelte@^5 tailwindcss@^4Setup
1. Wrap your app with <ThemeProvider>
In your root layout (e.g. src/routes/+layout.svelte):
<script lang="ts">
import { ThemeProvider, defaultDarkTheme } from 'desert-ui';
import 'desert-ui/styles/base.css';
let { children } = $props();
</script>
<ThemeProvider theme={defaultDarkTheme}>
{@render children()}
</ThemeProvider>2. Import Tailwind and use components
Your src/app.css (or wherever you import Tailwind):
@import 'tailwindcss';Then anywhere:
<script>
import { Button, Card, SectionHeading, MetricCard } from 'desert-ui';
</script>
<Card>
<SectionHeading title="Dashboard" />
<MetricCard label="Active users" value="12,480" hint="+4.2% vs last month" />
<Button variant="accent">Go</Button>
</Card>Theming
A Theme is a typed object. <ThemeProvider> walks it and writes CSS custom
properties (--ds-*) onto its wrapping element. Every component styles itself
with those variables — never with raw Tailwind colors. Swapping the theme prop
is instant.
18 built-in themes
// Dark themes
import {
defaultDarkTheme, // warm stone + emerald
claudeDarkTheme, // terracotta + warm black
merchantTheme, // commerce green + cool black
atlasTheme, // sky blue + graphite
cipherTheme, // mint green + true black
sourceTheme, // link blue + Primer navy
emberTheme, // warm orange + charcoal
amethystTheme, // violet + cool gray
reefTheme, // teal + slate blue
carbonTheme, // IBM red + true black
auroraTheme, // aurora green + dark blue
} from 'desert-ui';
// Light themes
import {
sandLightTheme, // sand + emerald
figmintTheme, // parchment cream + mint
blossomTheme, // pink-white + rose
linenTheme, // warm beige + forest green
glacierTheme, // ice white + ocean blue
meadowTheme, // pale mint + leaf green
ivoryTheme, // warm white + indigo
} from 'desert-ui';Author your own
import type { Theme } from 'desert-ui';
export const brandX: Theme = {
name: 'brand-x',
isDark: true,
tokens: {
surface: { /* ... */ },
text: { /* ... */ },
accent: { /* ... */ },
// ...see src/lib/theme/types.ts for the full shape
}
};Switch themes at runtime
<script>
import { ThemeProvider, defaultDarkTheme, sandLightTheme } from 'desert-ui';
let active = $state(defaultDarkTheme);
</script>
<button onclick={() => (active = sandLightTheme)}>Light</button>
<button onclick={() => (active = defaultDarkTheme)}>Dark</button>
<ThemeProvider theme={active}>
<!-- the whole app re-tokenizes instantly -->
</ThemeProvider>Responsiveness
breakpoint.isMobile is a rune-backed getter that flips at 1024px. Layout
components (Sidebar, MobileDrawer, MobileTopbar) use CSS queries
internally. Overlays (Modal, BottomSheet) use the rune directly to pick a
presentation style.
<script>
import { breakpoint, Modal, BottomSheet } from 'desert-ui';
let open = $state(false);
</script>
{#if breakpoint.isMobile}
<BottomSheet bind:open>
<!-- ... -->
</BottomSheet>
{:else}
<Modal bind:open>
<!-- ... -->
</Modal>
{/if}Loading state
loadingState is a dual-counter store: pending API requests and route
navigations both push the top-edge <NProgressBar /> into its "loading" mode,
with debounced show/hide so it never flashes.
Wire it into your API client and router once:
import { loadingState } from 'desert-ui';
export async function apiRequest(url: string, init?: RequestInit) {
loadingState.beginRequest();
try {
return await fetch(url, init);
} finally {
loadingState.endRequest();
}
}<!-- routes/+layout.svelte -->
<script>
import { NProgressBar, loadingState } from 'desert-ui';
import { beforeNavigate, afterNavigate } from '$app/navigation';
beforeNavigate(() => loadingState.startRoute());
afterNavigate(() => loadingState.endRoute());
</script>
<NProgressBar />Available components
Primitives (9)
AccessStateCard · BrandMark · Button · DesertLogo · Flag · Icon · LinkifiedText · SectionHeading · Typography
Feedback (6)
LoadingPanel · NProgressBar · ProgressBar · SkeletonBlock · Spinner · TableSkeleton
Data display (8)
Accordion · Card · EmptyState · MetricCard · Pill · SegmentedBar · StatusBadge · Table
Layout (7)
AppShell · Breadcrumb · MobileDrawer · MobileTopbar · PageHeader · Sidebar · SidebarNav
Overlays (4)
BottomSheet · Dropdown · Modal · SpotlightSearch
Forms (10)
ChoiceCard · Combobox · DatePicker · DateRangePicker · FieldLabel · FileDropzone · Select · TextInput · Textarea · TimePicker
Development
npm install # install dependencies
npm run dev # playground at http://localhost:5173
npm run storybook # Storybook at http://localhost:6006
npm run check # svelte-check + TypeScript
npm run build # build + publint-checked package in dist/Project layout
src/lib/
├── primitives/ — leaf components (Button, Icon, Typography, …)
├── feedback/ — loaders, skeletons, progress
├── data-display/ — cards, badges, tables, accordions
├── layout/ — AppShell, Sidebar, MobileDrawer
├── overlays/ — Modal, BottomSheet, Dropdown
├── forms/ — inputs, selects, date/time pickers, dropzones
├── stores/ — breakpoint, loadingState
├── theme/ — ThemeProvider, types, 18 built-in themes
├── styles/ — base.css (resets + skeleton + animations)
└── utils/ — cn()Every component has a .stories.svelte file beside it — run npm run storybook
to explore variants interactively and preview themes via the toolbar.
Philosophy
- Tokens over classes. Components reference
var(--ds-*), neverbg-emerald-500. Themes are the only place colors live. - One breakpoint.
1024pxis the one line that splits "mobile" from "desktop". Consistency > granularity. - Snippets over slot prop hell. Composition uses Svelte 5 snippets so consumers fully control layout bits like workspace switchers, sidebar footers, and modal CTAs.
- No surprises. No hidden global state, no side-effectful imports, no
component-local
fetch. Everything observable is a store you can wire into.
License
MIT
