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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@makolabs/ripple

v1.5.0

Published

Simple Svelte 5 powered component library ✨

Readme

Ripple UI

A modern, standardized Svelte 5 component library designed for simplicity, consistency, and AI-friendly usage patterns.

Key Features

  • Standardized API with consistent prop naming and patterns across components
  • Enum-based properties for predictable component customization
  • Strong TypeScript support with comprehensive type definitions
  • Utility-first approach built with TailwindCSS
  • Accessible components adhering to modern web standards
  • Simplified component consumption ideal for both human and AI developers

Getting started

Install the project

npm i @makolabs/ripple

Usage

Import ripple UI components

<script lang="ts">
	import { Button, Card, Modal } from '@makolabs/ripple';
</script>

<div class="px-12 pt-12">
	<Card title="Hello World" color="warning">
		<p>This is a card component</p>
	</Card>
</div>

Paste the following CSS import code in app.css

@source '../node_modules/@makolabs/ripple';

@theme {
	/* Default (default) */
	--color-default-50: oklch(0.984 0.003 247.858);
	--color-default-100: oklch(0.96 0.006 247.858);
	--color-default-200: oklch(0.91 0.008 247.858);
	--color-default-300: oklch(0.85 0.01 247.858);
	--color-default-400: oklch(0.76 0.012 247.858);
	--color-default-500: oklch(0.65 0.015 247.858);
	--color-default-600: oklch(0.54 0.018 247.858);
	--color-default-700: oklch(0.45 0.015 247.858);
	--color-default-800: oklch(0.35 0.012 247.858);
	--color-default-900: oklch(0.25 0.01 247.858);
	--color-default-950: oklch(0.15 0.008 247.858);

	/* Primary (Blue) */
	--color-primary-50: oklch(0.97 0.025 250);
	--color-primary-100: oklch(0.94 0.035 250);
	--color-primary-200: oklch(0.89 0.055 250);
	--color-primary-300: oklch(0.82 0.075 250);
	--color-primary-400: oklch(0.74 0.095 250);
	--color-primary-500: oklch(0.65 0.115 250);
	--color-primary-600: oklch(0.55 0.125 250);
	--color-primary-700: oklch(0.45 0.115 250);
	--color-primary-800: oklch(0.35 0.095 250);
	--color-primary-900: oklch(0.25 0.075 250);
	--color-primary-950: oklch(0.15 0.055 250);

	/* Secondary (Slate) */
	--color-secondary-50: oklch(0.97 0.02 255);
	--color-secondary-100: oklch(0.94 0.03 255);
	--color-secondary-200: oklch(0.89 0.04 255);
	--color-secondary-300: oklch(0.82 0.05 255);
	--color-secondary-400: oklch(0.74 0.06 255);
	--color-secondary-500: oklch(0.65 0.07 255);
	--color-secondary-600: oklch(0.55 0.065 255);
	--color-secondary-700: oklch(0.45 0.055 255);
	--color-secondary-800: oklch(0.35 0.045 255);
	--color-secondary-900: oklch(0.25 0.035 255);
	--color-secondary-950: oklch(0.15 0.025 255);

	/* Info (Sky) */
	--color-info-50: oklch(0.97 0.025 220);
	--color-info-100: oklch(0.94 0.04 220);
	--color-info-200: oklch(0.89 0.06 220);
	--color-info-300: oklch(0.82 0.085 220);
	--color-info-400: oklch(0.74 0.105 220);
	--color-info-500: oklch(0.65 0.125 220);
	--color-info-600: oklch(0.55 0.115 220);
	--color-info-700: oklch(0.45 0.105 220);
	--color-info-800: oklch(0.35 0.085 220);
	--color-info-900: oklch(0.25 0.065 220);
	--color-info-950: oklch(0.15 0.045 220);

	/* Success (Green) */
	--color-success-50: oklch(0.97 0.025 145);
	--color-success-100: oklch(0.94 0.04 145);
	--color-success-200: oklch(0.89 0.06 145);
	--color-success-300: oklch(0.82 0.08 145);
	--color-success-400: oklch(0.74 0.1 145);
	--color-success-500: oklch(0.65 0.12 145);
	--color-success-600: oklch(0.55 0.11 145);
	--color-success-700: oklch(0.45 0.1 145);
	--color-success-800: oklch(0.35 0.08 145);
	--color-success-900: oklch(0.25 0.06 145);
	--color-success-950: oklch(0.15 0.04 145);

	/* Warning (Yellow) */
	--color-warning-50: oklch(0.97 0.025 90);
	--color-warning-100: oklch(0.94 0.045 90);
	--color-warning-200: oklch(0.89 0.065 90);
	--color-warning-300: oklch(0.82 0.085 90);
	--color-warning-400: oklch(0.74 0.105 90);
	--color-warning-500: oklch(0.65 0.125 90);
	--color-warning-600: oklch(0.55 0.115 90);
	--color-warning-700: oklch(0.45 0.105 90);
	--color-warning-800: oklch(0.35 0.085 90);
	--color-warning-900: oklch(0.25 0.065 90);
	--color-warning-950: oklch(0.15 0.045 90);

	/* Danger (Red) */
	--color-danger-50: oklch(0.97 0.025 25);
	--color-danger-100: oklch(0.94 0.045 25);
	--color-danger-200: oklch(0.89 0.065 25);
	--color-danger-300: oklch(0.82 0.085 25);
	--color-danger-400: oklch(0.74 0.105 25);
	--color-danger-500: oklch(0.65 0.125 25);
	--color-danger-600: oklch(0.55 0.115 25);
	--color-danger-700: oklch(0.45 0.105 25);
	--color-danger-800: oklch(0.35 0.085 25);
	--color-danger-900: oklch(0.25 0.065 25);
	--color-danger-950: oklch(0.15 0.045 25);
}

Design Philosophy

Ripple UI was built with a focus on consistency and standardization. Every component follows the same patterns for customization:

Standardized Enums

Components use standardized enums for colors, sizes, and variants:

// Colors available for most components
Color.DEFAULT; // 'default'
Color.PRIMARY; // 'primary'
Color.SECONDARY; // 'secondary'
Color.INFO; // 'info'
Color.SUCCESS; // 'success'
Color.WARNING; // 'warning'
Color.DANGER; // 'danger'

// Sizes available for most components
Size.XS; // 'xs'
Size.SM; // 'sm'
Size.BASE; // 'base'
Size.LG; // 'lg'
Size.XL; // 'xl'
Size.XXL; // '2xl'

Consistent Props Pattern

All components follow a consistent props pattern with predictable naming:

  • color: Component color theme (using the Color enum)
  • size: Component size (using the Size enum)
  • class: Custom CSS classes for the component
  • Event handlers with on prefix (e.g., onclick, onchange)
  • Element-specific class props named with component + 'class' (e.g., titleclass, bodyclass)

Component Variants

Most components in Ripple UI support variants to customize their appearance. Here are some examples:

Button Variants

Buttons come with different variants, colors, sizes, and shapes:

<script lang="ts">
	import { Button, Color, Size } from '@makolabs/ripple';
</script>

<!-- Different button variants -->
<Button variant="solid" color={Color.PRIMARY}>Solid Button</Button>
<Button variant="outline" color={Color.SECONDARY}>Outline Button</Button>
<Button variant="ghost" color={Color.DANGER}>Ghost Button</Button>
<Button variant="link" color={Color.INFO}>Link Button</Button>

<!-- Button with onclick handler -->
<Button color={Color.SUCCESS} onclick={() => console.log('Button clicked')}>Click Me</Button>

<!-- Button as link -->
<Button href="https://example.com" target="_blank" color={Color.PRIMARY}>Visit Website</Button>

<!-- Button sizes -->
<Button size={Size.XS}>Extra Small</Button>
<Button size={Size.SM}>Small</Button>
<Button size={Size.BASE}>Base</Button>
<Button size={Size.LG}>Large</Button>
<Button size={Size.XL}>Extra Large</Button>
<Button size={Size.XXL}>2X Large</Button>

<!-- Button variants with different colors -->
<Button variant="solid" color={Color.PRIMARY}>Primary Solid</Button>
<Button variant="solid" color={Color.DANGER}>Danger Solid</Button>
<Button variant="outline" color={Color.SUCCESS}>Success Outline</Button>
<Button variant="ghost" color={Color.WARNING}>Warning Ghost</Button>
<Button variant="link" color={Color.INFO}>Info Link</Button>

Modal Variants

Modals with different sizes and custom content:

<script lang="ts">
	import { Modal, Button, Size } from '@makolabs/ripple';
	let isOpen = false;
</script>

<Button onclick={() => (isOpen = true)}>Open Modal</Button>

<!-- Basic modal -->
<Modal open={isOpen} title="Basic Modal" size={Size.BASE} onClose={() => (isOpen = false)}>
	<p>Modal content goes here</p>
</Modal>

<!-- Modal with different size -->
<Modal open={isOpen} title="Large Modal" size={Size.XL} onClose={() => (isOpen = false)}>
	<p>This modal is larger and provides more content space</p>
</Modal>

<!-- Modal with custom header and footer -->
<Modal open={isOpen} onClose={() => (isOpen = false)} size={Size.BASE}>
	<svelte:fragment slot="header">
		<div class="flex items-center">
			<svg
				xmlns="http://www.w3.org/2000/svg"
				width="24"
				height="24"
				fill="currentColor"
				viewBox="0 0 16 16"
			>
				<path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
			</svg>
			<h3 class="text-lg font-medium">Custom Header</h3>
		</div>
	</svelte:fragment>

	<p>Modal with custom header and footer</p>

	<svelte:fragment slot="footer">
		<div class="flex justify-end space-x-2">
			<Button variant="outline" onclick={() => (isOpen = false)}>Cancel</Button>
			<Button color={Color.PRIMARY}>Save Changes</Button>
		</div>
	</svelte:fragment>
</Modal>

<!-- TODO: Remove position prop from Modal component in future versions -->

Drawer Component

Drawers can slide in from different edges of the screen:

<script lang="ts">
	import { Drawer, Button } from '@makolabs/ripple';
	let isDrawerOpen = false;
</script>

<Button onclick={() => (isDrawerOpen = true)}>Open Drawer</Button>

<Drawer open={isDrawerOpen} position="right" onClose={() => (isDrawerOpen = false)}>
	<div class="p-4">
		<h3 class="mb-4 text-lg font-medium">Drawer Title</h3>
		<p class="mb-4">This is a drawer that slides in from the side of the screen.</p>
		<Button onclick={() => (isDrawerOpen = false)}>Close Drawer</Button>
	</div>
</Drawer>

PageHeader Component

A component for consistent page headers:

<script lang="ts">
	import { PageHeader, Button, Color } from '@makolabs/ripple';

	const breadcrumbs = [
		{ label: 'Dashboard', href: '#' },
		{ label: 'Projects', href: '#' },
		{ label: 'Current Project' }
	];
</script>

<PageHeader
	title="Project Dashboard"
	description="View and manage your project details"
	{breadcrumbs}
>
	<svelte:fragment slot="actions">
		<Button color={Color.PRIMARY}>New Project</Button>
	</svelte:fragment>
</PageHeader>

Card Variants

Cards can be customized with different styles:

<script lang="ts">
	import { Card, StatsCard, Color } from '@makolabs/ripple';
</script>

<Card title="Basic Card" color={Color.PRIMARY}>
  <p>Card content goes here</p>
</Card>

<StatsCard
  label="Monthly Sales"
  value="$865,000"
  previousValue="$750,000"
  previousValuePrefix="vs"
  trend={15.3}
  color={Color.SUCCESS}
  chartData={[20, 25, 30, 22, 35, 40, 38, 45, 50]}
  icon={
    <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 16 16">
      <path d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
    </svg>
  }
/>

Table Component

Tables for displaying structured data with pagination and sorting:

<script lang="ts">
	import { Table, Color, Size } from '@makolabs/ripple';

	let data = [
		{ id: 1, name: 'John Doe', email: '[email protected]', status: 'Active' },
		{ id: 2, name: 'Jane Smith', email: '[email protected]', status: 'Inactive' },
		{ id: 3, name: 'Robert Johnson', email: '[email protected]', status: 'Active' }
	];

	const columns = [
		{ key: 'name', label: 'Name', sortable: true },
		{ key: 'email', label: 'Email', sortable: true },
		{ key: 'status', label: 'Status' }
	];

	let selected = [];
	let sort = { column: 'name', direction: 'asc' };
</script>

<Table
	{data}
	{columns}
	color={Color.PRIMARY}
	size={Size.BASE}
	pageSize={10}
	selectable={true}
	bind:selected
	bind:sort
	striped={true}
/>

Tab Component

Tabs for organizing content into different views:

<script lang="ts">
	import { TabGroup, TabContent, Color, Size } from '@makolabs/ripple';

	const tabs = [
		{ value: 'overview', label: 'Overview' },
		{ value: 'details', label: 'Details' },
		{ value: 'settings', label: 'Settings' }
	];

	let activeTab = 'overview';

	function handleTabChange(value) {
		console.log(`Tab changed to ${value}`);
	}
</script>

<TabGroup
	{tabs}
	bind:selected={activeTab}
	onchange={handleTabChange}
	color={Color.PRIMARY}
	size={Size.BASE}
>
	<TabContent value="overview" persisted>
		<p>Overview content here</p>
	</TabContent>

	<TabContent value="details" persisted>
		<p>Details content here</p>
	</TabContent>

	<TabContent value="settings" persisted>
		<p>Settings content here</p>
	</TabContent>
</TabGroup>

Badge Component

Badges for displaying statuses and counts:

<script lang="ts">
	import { Badge, Color, Size } from '@makolabs/ripple';
</script>

<Badge color={Color.PRIMARY} size={Size.BASE}>New</Badge>
<Badge color={Color.SUCCESS}>Success</Badge>
<Badge color={Color.WARNING}>Warning</Badge>
<Badge color={Color.DANGER}>43</Badge>

Select Component

Dropdown selector for choosing from a list of options:

<script lang="ts">
	import { Select, Size } from '@makolabs/ripple';

	const items = [
		{ label: 'Option 1', value: 'option1' },
		{ label: 'Option 2', value: 'option2' },
		{ label: 'Option 3', value: 'option3', disabled: true },
		{ label: 'Option 4', value: 'option4' }
	];

	let selected = 'option1';

	function handleSelect(event) {
		console.log('Selected:', event.value);
	}
</script>

<Select {items} bind:value={selected} class="w-64" size={Size.BASE} onselect={handleSelect} />

Dropdown Component

Menu dropdown for actions and navigation:

<script lang="ts">
	import { Dropdown, Size } from '@makolabs/ripple';
	import FluentChevronDown16Filled from '$icons/FluentChevronDown16Filled.svelte';

	const sections = [
		{
			items: [
				{
					label: 'Edit',
					icon: FluentPen16Filled,
					onclick: () => console.log('Edit clicked')
				},
				{
					label: 'Duplicate',
					icon: FluentPenSparkle24Filled,
					onclick: () => console.log('Duplicate clicked')
				}
			]
		},
		{
			items: [
				{
					label: 'Delete',
					icon: FluentDelete24Filled,
					onclick: () => console.log('Delete clicked')
				}
			]
		}
	];
</script>

<Dropdown {sections} label="Actions" size={Size.BASE} icon={FluentChevronDown16Filled} />

Component Composition

Ripple UI components are designed to work together seamlessly:

<script lang="ts">
	import { Card, TabGroup, TabContent, Button, Color, Size } from '@makolabs/ripple';

	const tabs = [
		{ value: 'overview', label: 'Overview' },
		{ value: 'details', label: 'Details' },
		{ value: 'settings', label: 'Settings' }
	];

	let activeTab = 'overview';
</script>

<Card title="Project Information" color={Color.PRIMARY}>
	<TabGroup {tabs} bind:selected={activeTab} color={Color.INFO} size={Size.BASE}>
		<TabContent value="overview">
			<p>Project overview content here...</p>
			<Button variant="solid" color={Color.SUCCESS} size={Size.SM}>Take Action</Button>
		</TabContent>

		<TabContent value="details">
			<p>Project details content here...</p>
		</TabContent>

		<TabContent value="settings">
			<p>Project settings content here...</p>
		</TabContent>
	</TabGroup>
</Card>

Latest Updates

Ripple UI now exports all components from a central entry point, making it easier to import components:

<script lang="ts">
	import { Button, Modal, Card, Table, Select, Dropdown } from '@makolabs/ripple';
</script>

You can still import specific component types when needed:

<script lang="ts">
	import { Button, type ButtonProps } from '@makolabs/ripple';

	// Create a custom button with specific props
	const myButton: ButtonProps = {
		variant: 'outline',
		color: 'primary',
		size: 'lg',
		rounded: 'xl'
	};
</script>

<Button {...myButton}>Custom Button</Button>

Testing

Ripple UI uses Vitest for unit and component testing. The test suite includes tests for core library components using Svelte 5's native testing APIs.

Running Tests Locally

Run all tests once:

npm run test:unit -- --run

Run tests in watch mode (for development):

npm run test:unit

Run all tests (unit + e2e):

npm test

Writing Tests

Component tests use Svelte 5's mount and unmount APIs for testing components. Here's an example test for a Button component:

import { flushSync, mount, unmount } from 'svelte';
import { expect, test } from 'vitest';
import ButtonTestWrapper from './ButtonTestWrapper.svelte';

test('renders as a button with slot content and respects disabled prop', () => {
	const component = mount(ButtonTestWrapper, {
		target: document.body,
		props: {
			disabled: true,
			text: 'Click me'
		}
	});

	const btn = document.body.querySelector('button') as HTMLButtonElement;
	expect(btn).toBeTruthy();
	expect(btn.disabled).toBe(true);
	expect(btn.textContent).toContain('Click me');

	unmount(component);
});

For components with snippet props (like children or footer), create a test wrapper component that accepts simple props and renders them as slots.

Test Configuration

Tests are configured using Vitest workspaces in vite.config.ts:

  • Client tests: Run in jsdom environment, include files matching src/**/*.svelte.{test,spec}.{js,ts}
  • Server tests: Run in node environment, include files matching src/**/*.{test,spec}.{js,ts} (excluding .svelte tests)

The setup file vitest-setup-client.ts includes:

  • @testing-library/jest-dom matchers for enhanced assertions
  • matchMedia mock for jsdom compatibility with Svelte 5

Continuous Integration

For CI environments (GitHub Actions, etc.), use:

- name: Install dependencies
  run: npm ci

- name: Run tests
  run: npm run test:unit -- --run