npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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/ripple

Table of contents


Quick start

1. Install

npm i @makolabs/ripple

2. 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 test

Component 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.