@makolabs/ripple
v3.5.0
Published
Simple Svelte 5 powered component library ✨
Readme
Ripple UI
A Svelte 5 + TailwindCSS 4 component library for building data-dense product UIs (dashboards, admin tools, internal apps). Built on tailwind-variants with consistent naming, comprehensive TypeScript types, and per-prop JSDoc that lets LLMs and IDE tooltips answer "how do I use this?" without leaving the editor.
npm i @makolabs/rippleTable of contents
- Quick start
- Theme tokens
- Component catalog
- Conventions
- Selected examples
- Server-side helpers
- Testing
- Troubleshooting
Quick start
1. Install
npm i @makolabs/ripple2. Wire up your CSS
Tailwind 4 does not scan node_modules automatically. Add a @source directive plus the @theme color tokens to your app.css:
@import 'tailwindcss';
@source '../node_modules/@makolabs/ripple';
@theme {
/* Ripple expects 7 color scales — see "Theme tokens" below for the full set */
--color-primary-500: oklch(0.65 0.115 250);
--color-success-500: oklch(0.65 0.12 145);
--color-danger-500: oklch(0.65 0.125 25);
/* …etc */
}3. Use a component
<script lang="ts">
import { Button, Card } from '@makolabs/ripple';
let count = $state(0);
</script>
<Card title="Counter" color="primary">
<p>Clicks: {count}</p>
<Button onclick={() => count++}>Increment</Button>
</Card>That's it — no provider component, no global CSS reset, no theme context.
Theme tokens
Ripple components use seven semantic color scales. Each scale needs steps 50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950.
| Scale | Use for | Example component |
| ----------- | ------------------------------------------ | ----------------- |
| default | Neutral text, borders, surfaces | Disabled buttons |
| primary | Brand color, primary actions, focus rings | "Save" button |
| secondary | Alternate brand color (counter to primary) | Sidebar accent |
| info | Informational tints (often blue/cyan) | Alert (info) |
| success | Success states, positive metrics | Alert (success) |
| warning | Caution, pending states (yellow/amber) | Alert (warning) |
| danger | Destructive actions, errors (red) | Delete button |
A complete @theme block with all seven scales is in docs/THEME.md (or copy-paste from this README's git history). Pick any palette — Ripple just consumes the token names.
Component catalog
All components are exported from the package root: import { Component } from '@makolabs/ripple'. Per-prop JSDoc with @example blocks is on every type, so IDE tooltips and AI assistants give you the right signature immediately.
Forms
| Component | Use for |
| ------------------ | ------------------------------------------------- |
| Input | Single-line text/email/number/etc. |
| Textarea | Multi-line text with autoGrow + character counter |
| Checkbox | Boolean selection ("I accept the terms") |
| Toggle | Boolean switch (on/off settings) |
| RadioGroup | Pick one from a short list |
| Select | Single/multi-select dropdown with optional search |
| ComboBox | Searchable select with free-typing + filter |
| SegmentedControl | Compact pill-style picker (2-5 options) |
| MarketSelector | Specialised SegmentedControl with country flags |
| Slider | Numeric or range slider, with optional ticks |
| NumberInput | Number input with currency/unit dropdown |
| Tags | List of short string tags with autocomplete |
| DatePicker | Single date picker with calendar popover |
| DateRange | Two-date (from/to) picker |
| Calendar | Standalone month-view calendar (single + range) |
| Form | sveltekit-superforms wrapper |
Layout
| Component | Use for |
| --------------------------------- | ------------------------------------------------------------------- |
| Card | Generic bordered container |
| MetricCard | KPI-focused card (big number + optional progress) |
| RankedCard | Leaderboard-style ranked list |
| Table | Data table with sorting/selection/pagination |
| Cells | Pre-built cell renderers (Cells.Currency, Cells.DateCell, etc.) |
| TabGroup + TabContent + Tab | Tabbed panels |
| Navbar | Top navigation bar |
| Sidebar | Left rail navigation, collapsible |
| ActivityList | Feed of recent events / audit trail |
| Pipeline | Chevron stage bar (sales pipeline, funnel) |
| Stepper | Multi-step form wizard |
| PageHeader | Title + subtitle + breadcrumbs + actions |
| Breadcrumbs | Hierarchical link trail |
Elements
| Component | Use for |
| ------------- | ---------------------------------------- |
| Button | Buttons + anchor links (set href) |
| Badge | Pills for statuses, counts, tags |
| Alert | Inline page-level banner |
| Accordion | Collapsible section |
| Spinner | Indeterminate loading indicator |
| Skeleton | Layout-preserving content placeholder |
| Progress | Linear progress bar (single + segmented) |
| EmptyState | Centered "nothing here" placeholder |
| Tooltip | Hover/focus-triggered text label |
| Popover | Generic anchored floating panel |
| Dropdown | Action menu attached to a trigger |
| ContextMenu | Right-click / long-press menu |
| Pagination | Page navigation controls |
Overlays
| Component | Use for |
| --------------------- | ------------------------------------ |
| Modal | Centered overlay dialog |
| Drawer | Side-anchored sliding panel |
| ConfirmDialog | "Are you sure?" wrapper around Modal |
| Toaster + toast() | Transient notifications |
Filters
| Component | Use for |
| ------------------ | ------------------------------------------------------ |
| CompactFilters | Collapsible filter bar with chip-summary mode |
| FilterPopover | Per-group dropdown pills (status, priority, dates) |
| FilterBar | Chip-based add/remove filters with "+ Add filter" menu |
| syncFiltersToUrl | Two-way bind selections to URL query params |
Charts
| Component | Use for |
| --------- | --------------------------------------------------------------------------------- |
| Chart | ECharts wrapper — line, bar, stacked-bar, horizontal-bar, area, pie, donut, mixed |
File handling
| Component / helper | Use for |
| -------------------- | ------------------------------------------------------------ |
| FileUpload | Drag-and-drop with single + multi-file + rich-mode progress |
| FilesPreview | List of uploaded files with delete |
| FileBrowser | Browse/select files on remote storage (S3, GDrive) |
| S3Adapter | Storage adapter for S3-compatible backends (DO Spaces, etc.) |
| GoogleDriveAdapter | Storage adapter for Google Drive |
| createS3Handlers | Server-side route handlers (@makolabs/ripple/server) |
AI / Chat
| Component | Use for |
| ----------------- | ---------------------------------------------- |
| AIChatInterface | Full chat UI with streaming + thinking content |
| MermaidRenderer | Render Mermaid diagrams from chat output |
| CodeRenderer | Syntax-highlighted code blocks |
User management
| Component | Use for |
| ---------------- | -------------------------------------------------------- |
| UserManagement | All-in-one user dashboard (table + modals + permissions) |
| UserTable | Just the user table |
| UserModal | Create/edit user form |
| UserViewModal | Read-only user details |
Helpers + variant utilities
| Helper | Purpose |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------- |
| cn | twMerge(clsx(…)) — conditional class composition |
| tv | tailwind-variants factory pre-configured with twMerge |
| buildTestId | Compose data-testid strings consistently |
| buttonVariants, badge, modal, drawer, breadcrumbs, card, metricCard, rankedCard, activityList, slider, segmentedTrack, dropdownMenu, selectTV, pipelineVariants, spinnerVariants, emptyStateVariants | Raw tv() outputs for consumers who want the styling without the component |
Conventions
Knowing five rules unlocks every component.
1. Color and Size enums
import { Color, Size } from '@makolabs/ripple';
Color.DEFAULT |
Color.PRIMARY |
Color.SECONDARY |
Color.INFO |
Color.SUCCESS |
Color.WARNING |
Color.DANGER;
Size.XS | Size.SM | Size.MD | Size.LG | Size.XL | Size.XXL;You can also pass the string literal (color="primary", size="md") — both are accepted because the enum values are plain strings.
A few components narrow Size to the variants their visual design supports (e.g. Tooltip uses 'sm' | 'md' | 'lg'). The narrowed types are exported from the same package.
2. Event handler naming
All component event-handler props are lowercase on-prefixed (Svelte 5 convention):
<Button onclick={save}>Save</Button>
<Modal onclose={() => (open = false)}>…</Modal>
<TabGroup onchange={(value) => goto(value)} />
<Slider oninput={(v) => (rate = v)} />Inside components, the lowercase prop is often aliased to a camelCase local for readability:
let { onclose: onClose = () => {}, ...rest } = $props();3. Class props
The root container takes class?: ClassValue. Sub-element class props use camelCase with a Class suffix:
<Modal
class="max-w-2xl"
titleClass="text-lg font-bold"
bodyClass="p-6"
footerClass="bg-default-50"
/>ClassValue accepts strings, arrays, and conditional objects (like clsx).
4. Snippets, not slots
Ripple is Svelte 5 — no <slot> tags anywhere. All content projection uses snippets:
<Modal bind:open title="Edit profile">
<ProfileForm />
{#snippet footer()}
<ModalFooter align="end">
<Button variant="outline" onclick={() => (open = false)}>Cancel</Button>
<Button onclick={save}>Save</Button>
</ModalFooter>
{/snippet}
</Modal>Snippets can also receive arguments — useful for per-row renderers in Table, per-step content in Stepper, etc.
5. testId everywhere
Every public component accepts an optional testId?: string and emits data-testid attributes scoped to its parts (data-testid="modal-dialog", data-testid="my-prefix-modal-body" when testId="my-prefix"). Use it for E2E tests.
Selected examples
Forms
<script lang="ts">
import { Form, Input, Textarea, RadioGroup, Button } from '@makolabs/ripple';
import { superForm } from 'sveltekit-superforms';
export let data;
const form = superForm(data.form);
</script>
<Form {form} method="POST">
<Input name="email" type="email" label="Email" required />
<RadioGroup
name="plan"
label="Plan"
options={[
{ value: 'free', label: 'Free' },
{ value: 'pro', label: 'Pro', description: '$10/mo' }
]}
/>
<Textarea name="notes" label="Notes" rows={3} maxLength={500} showCount />
<Button type="submit">Save</Button>
</Form>Tables + cells
Cells is a namespace import of pre-built snippets — Currency, Email, DateCell, Time, PhoneNumber, Percentage, Status. Hand them to column.cell to skip the boilerplate:
<script lang="ts">
import { Table, Cells } from '@makolabs/ripple';
import type { TableColumn } from '@makolabs/ripple';
type User = { id: string; name: string; email: string; createdAt: string; lastSpent: number };
const columns: TableColumn<User>[] = [
{ key: 'name', header: 'Name', sortable: true },
{ key: 'email', header: 'Email', cell: Cells.Email },
{ key: 'createdAt', header: 'Joined', cell: Cells.DateCell, align: 'right' },
{ key: 'lastSpent', header: 'Last spent', cell: Cells.Currency, align: 'right' }
];
</script>
<Table
{columns}
data={users}
{loading}
pageSize={25}
title="Customers"
onrowclick={(user) => goto(`/users/${user.id}`)}
/>Modals + dialogs
Three flavours: Modal (general), ConfirmDialog (yes/no), Drawer (side panel).
<script lang="ts">
import { ConfirmDialog, Button, Color } from '@makolabs/ripple';
let open = $state(false);
async function deleteAccount() {
await api.deleteAccount();
// dialog auto-closes when onconfirm resolves
}
</script>
<Button color={Color.DANGER} variant="outline" onclick={() => (open = true)}>Delete account</Button>
<ConfirmDialog
bind:open
title="Delete your account?"
message="This permanently removes your account, files, and history. This cannot be undone."
confirmLabel="Delete forever"
confirmColor={Color.DANGER}
onconfirm={deleteAccount}
/>Filters
FilterPopover for per-group dropdowns, FilterBar for add-as-needed chips, CompactFilters for collapsed-by-default detail panels. All three take the same filterGroups shape, including date-range groups and URL syncing:
<script lang="ts">
import { FilterPopover, syncFiltersToUrl } from '@makolabs/ripple';
import type { FilterGroup, FilterSelectionValue } from '@makolabs/ripple';
let selections = $state<Record<string, FilterSelectionValue>>({
status: 'all',
created: null
});
const filterGroups: FilterGroup[] = [
{
key: 'status',
label: 'Status',
tabs: [
{ value: 'all', label: 'All', count: 1247 },
{ value: 'active', label: 'Active', count: 823 },
{ value: 'archived', label: 'Archived', count: 112 }
]
},
{ key: 'created', label: 'Created', dateRange: true } // built-in calendar + presets
];
syncFiltersToUrl(
() => selections,
(v) => (selections = v),
{ dateRangeKeys: ['created'] }
);
</script>
<FilterPopover {filterGroups} bind:selections />Charts
<script lang="ts">
import { Chart, ChartColor } from '@makolabs/ripple';
const data = [
{ month: '2026-01', netCash: 250000 },
{ month: '2026-02', netCash: -120000 },
{ month: '2026-03', netCash: 60000 }
];
</script>
<Chart
{data}
config={{
xAxis: { dataKey: 'month' },
yAxis: [
{
dataKey: 'netCash',
label: 'Net cash (CHF)',
format: (v) => `CHF ${(v / 1000).toFixed(0)}K`
}
],
series: [
{
dataKey: 'netCash',
name: 'Monthly Net Value',
type: 'bar',
color: ChartColor.HEALTH
}
]
}}
height="320px"
/>Server-side helpers
Some components have a server-side companion. Import from @makolabs/ripple/server:
// src/routes/api/s3/list/+server.ts
import { createS3Handlers } from '@makolabs/ripple/server';
import { env } from '$env/dynamic/private';
const s3 = createS3Handlers({
bucket: env.S3_BUCKET,
region: env.S3_REGION,
accessKeyId: env.S3_ACCESS_KEY_ID,
secretAccessKey: env.S3_SECRET_ACCESS_KEY,
endpoint: env.S3_ENDPOINT // optional, e.g. for DigitalOcean Spaces
});
export const GET = ({ request }) => s3.list(request);Add the matching download route at src/routes/api/s3/download/+server.ts (s3.download). The client S3Adapter calls both endpoints — see the FileBrowser example below.
<script lang="ts">
import { FileBrowser, S3Adapter } from '@makolabs/ripple';
const adapter = new S3Adapter({ basePath: 'imports/' });
</script>
<FileBrowser {adapter} />Testing
Ripple uses Vitest with two workspace projects:
- client — jsdom environment, runs
*.svelte.{test,spec}.{js,ts} - server — node environment, runs other
*.{test,spec}.{js,ts}
npm test # run all tests
npm run test:unit # watch mode
npx vitest run path/to/file.test.ts # single testComponent test pattern
Svelte 5 snippets can't be passed as props from JS, so each tested component has a TestWrapper that takes scalar props and renders snippets internally:
// Button.svelte.test.ts
import { mount, unmount } from 'svelte';
import { expect, test } from 'vitest';
import ButtonTestWrapper from './ButtonTestWrapper.svelte';
test('renders disabled button with text', () => {
const component = mount(ButtonTestWrapper, {
target: document.body,
props: { disabled: true, text: 'Click me' }
});
const btn = document.body.querySelector('button') as HTMLButtonElement;
expect(btn.disabled).toBe(true);
expect(btn.textContent).toContain('Click me');
unmount(component);
});Use flushSync() from svelte after programmatic interactions that trigger reactive updates.
Troubleshooting
Components render without color
Tailwind 4 doesn't ship the color tokens Ripple uses. Add the @theme block to your app.css (see Theme tokens).
Component classes missing in production
Tailwind 4 doesn't scan node_modules automatically. Add to app.css:
@source '../node_modules/@makolabs/ripple';import 'foo' with …/$lib/foo.ts'` doesn't resolve
This codebase uses moduleResolution: 'nodenext' — local imports must use .js extensions even when importing .ts files:
import { cn } from '$lib/helper/cls.js'; // ✅
import { cn } from '$lib/helper/cls'; // ❌Storybook
The repo includes Storybook (npm run storybook, port 6006) with stories for every component and every interesting prop combination. Browse it for live examples beyond what's in this README.
License
MIT.
