@rshval/svelte-components
v1.2.6
Published
Reusable Svelte 5 UI component library (forms, table, map, notifications, drawers and stores).
Maintainers
Readme
@rshval/svelte-components
Reusable UI library for Svelte 5.
Package includes:
- base UI components (
Button,InputField,Select,Modal,Toast,Alert); - composite components (
Table,InputPhone,Drawer,Notifications); - map components built on
mapbox-gl; - helpers and plugins (
api,geoserviceApi,storage*); - ready-to-use Svelte stores for session, account, network, geolocation, and device info.
Project positioning
This library is primarily developed and used by the author in personal projects.
What this means in practice:
- there is currently no goal to turn this package into a large universal UI library with a full standalone docs site in the near term;
- API, component set, and dependency list may evolve as real product needs change;
- the library is kept up to date for the author’s active projects and updated in a timely manner;
- improvements, fixes, and suggestions are welcome via Pull Request.
About Storybook and examples
- Storybook in this repository is still incomplete and needs further work.
- For integration scenarios, prefer Svelte sandbox / a local SvelteKit sandbox.
- The README captures working patterns and should be used as the primary reference.
Svelte Playground note
If you see Failed to import $app@latest, it means the example contains SvelteKit-only aliases ($app/*) but is opened in plain Svelte Playground.
Even with the newest Svelte version selected in Playground, $app/* remains unavailable there because it is provided by SvelteKit runtime, not by the core svelte package.
Use one of these options:
- run the example in a SvelteKit project/sandbox;
- replace
$app/*imports with environment-agnostic alternatives before running in plain Svelte Playground.
Installation
npm i @rshval/svelte-componentsAlso make sure your project has compatible library dependencies installed:
@popperjs/core— runtime dependency (installed automatically with the package);svelte,@tiptap/*— peer dependencies that must stay compatible in consumer apps.@sveltejs/kitis optional and only needed in consumer apps that use SvelteKit-specific APIs.
Styling requirements (Tailwind + DaisyUI)
This library currently relies on utility and component classes from Tailwind CSS 4 and DaisyUI 5 (for example btn, btn-primary, input, drawer, modal).
If your app does not include these plugins, components will still render, but visual styles will be incomplete.
Minimum setup in consumer app styles:
@import 'tailwindcss';
@plugin 'daisyui';Quick README navigation
Quick start
<script lang="ts">
import { Button, InputField, Table } from '@rshval/svelte-components';
let value = $state('');
const rows = [{ id: '1', name: 'Alice' }];
const columns = [{ id: 'name', title: 'Name' }];
</script>
<InputField bind:value label="Name" placeholder="Enter name" />
<Button>Save</Button>
<Table {rows} {columns} />Component usage
Below are working patterns aligned with the current component implementations and exported API.
Modal
Use-case: confirmations, forms, and content cards with user actions.
For programmatic control (
showModal()/close()),bind:elementis required; otherwise consumers have no reference to the internaldialogelement.
Props / Events / Bindings
| Type | Fields |
| -------- | ---------------------------------------------------------------------------------------------- |
| Props | title, noActions, btnDisabled, classBox, class, styleBox, noAutoClose, btnText |
| Events | onclose |
| Bindings | bind:element, bind:send |
Basic example
<script lang="ts">
import { Modal } from '@rshval/svelte-components';
let modalElem: HTMLDialogElement | null = null;
</script>
<button class="btn" onclick={() => modalElem?.showModal()}>Open</button>
<Modal bind:element={modalElem} title="Title" noActions>
<div class="p-4">Modal content</div>
</Modal>Programmatic control (production-like)
<script lang="ts">
import { Modal, Button } from '@rshval/svelte-components';
let modalElem: HTMLDialogElement | null = null;
</script>
<Button onclick={() => modalElem?.showModal()}>Open</Button>
<Modal bind:element={modalElem} title="Title" noActions btnDisabled classBox="max-w-xl">
<div class="p-4">
Modal content
<div class="mt-4">
<Button onclick={() => modalElem?.close()} class="btn-ghost">Close</Button>
</div>
</div>
</Modal>Toast
Use-case: quick notifications for successful/error actions.
Props / Events / Bindings
| Type | Fields |
| -------- | ---------------- |
| Props | items, class |
| Events | onclose(item) |
| Bindings | none |
Input variants
- Current API:
itemsarray. - Legacy pattern:
{ type, message }object mapped toitemsin caller code.
Current example
<script lang="ts">
import { Toast } from '@rshval/svelte-components';
let toast: { type: 'success' | 'info' | 'alert'; message: string } | null = {
type: 'success',
message: 'Saved'
};
</script>
{#if toast}
<Toast items={[toast]} onclose={() => (toast = null)} />
{/if}Legacy-compatible example (type + message)
<script lang="ts">
import { Toast } from '@rshval/svelte-components';
let toast: { type: 'success' | 'info' | 'alert'; message: string } | null = {
type: 'info',
message: 'Done'
};
</script>
{#if toast}
<Toast items={[{ type: toast.type, message: toast.message }]} onclose={() => (toast = null)} />
{/if}
typeandmessageas standalone props are not supported by currentToast; for backward compatibility map them intoitems.
Table stack: TableFilters + Table + TablePagination
Use-case: list pages (orders/events/customers) with filters and pagination.
Table
| Type | Fields |
| -------- | ---------------------------------------------------------------------- |
| Props | columns, rows, hover, zebra, class, selected, ths, trs |
| Events | none (row selection is done via selected) |
| Bindings | bind:rows, bind:columns, bind:selected |
TableFilters
| Type | Fields |
| -------- | -------------------------------------- |
| Props | title, count, bodyClass, class |
| Events | none |
| Snippets | #snippet actions() + children |
TablePagination
| Type | Fields |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- |
| Props | total, page, limit, limitOptions, onPrev, onNext, onLimitChange, canPrev, canNext, summary, pageLabel, showLimit, class |
| Events | callback props |
| Bindings | none |
End-to-end example
<script lang="ts">
import { Table, TableFilters, TablePagination, Button } from '@rshval/svelte-components';
import ActionButtons from '$lib/components/ActionButtons.svelte';
let rows: any[] = [];
function openEdit(id: string) {
console.log('edit', id);
}
function doDelete(id: string) {
console.log('delete', id);
}
const columns = [
{ id: 'id', title: 'ID', tpl: (r: any) => r._id },
{ id: 'title', title: 'Title', tpl: (r: any) => r.title },
{
id: 'actions',
title: '',
component: ActionButtons,
props: {
onEdit: openEdit,
onDelete: doDelete
},
propsFn: (r: object) => {
return {
row: r
};
}
}
];
let total = 0;
let pageNumber = 1;
let limit = 10;
function resetFilters() {
pageNumber = 1;
load();
}
function load() {
// load rows/total
}
</script>
<TableFilters title="Filters" count={rows.length} bodyClass="grid grid-cols-1 gap-3 md:grid-cols-4">
{#snippet actions()}
<Button class="btn-ghost btn-sm" onclick={resetFilters}>Reset</Button>
{/snippet}
<input class="input-bordered input input-sm" placeholder="Search" />
<Button class="btn-sm btn-primary" onclick={load}>Apply</Button>
</TableFilters>
<Table {columns} {rows} hover class="table-sm" />
<TablePagination
{total}
page={pageNumber}
{limit}
limitOptions={[10, 20, 50]}
onLimitChange={(value) => {
pageNumber = 1;
limit = value;
load();
}}
onPrev={() => {
pageNumber -= 1;
load();
}}
onNext={() => {
pageNumber += 1;
load();
}}
canPrev={pageNumber > 1}
canNext={pageNumber * limit < total}
/>Example action-cell component (ActionButtons.svelte)
<!-- Action component for table cell -->
<script lang="ts">
import { Button } from '@rshval/svelte-components';
import IconEdit from '@tabler/icons-svelte-runes/icons/edit';
import IconTrash from '@tabler/icons-svelte-runes/icons/trash';
let { onEdit, onDelete, row, propsFn } = $props();
$effect(() => {
console.log(propsFn, row);
});
function handleEdit() {
onEdit(row._id);
}
function handleDelete() {
onDelete(row._id);
}
</script>
<div class="flex gap-2">
<Button class="btn-sm" onclick={handleEdit}><IconEdit size={16} /></Button>
<Button class="btn-sm btn-error" onclick={handleDelete}><IconTrash size={16} /></Button>
</div>How it works:
propsfrom the column definition are passed as regular component inputs (onEdit,onDelete).propsFn(row)is executed for each row and adds dynamic inputs (for example,row).- Inside the component, values are received via
let { ... } = $props(). - As a result,
ActionButtonsreceives both callbacks and current row data, then can callonEdit(row._id)andonDelete(row._id).
Form components
InputField
Use-case: text inputs and password fields with toggle.
| Type | Fields |
| -------- | --------------------------------------------------------------------------------------------------------------------------------------------------- |
| Props | value, label, type, passwordToggle, class, id + HTML attributes (placeholder, autocomplete, name, spellcheck, maxlength etc.) |
| Events | oninput, onchange, onfocus |
| Bindings | bind:value |
<InputField bind:value={title} placeholder="Title" />
<InputField
value={smtpHostDraft}
type="text"
oninput={(e: any) => (smtpHostDraft = String(e.currentTarget?.value || ''))}
/>InputPhone
Use-case: phone input with country selector.
| Type | Fields |
| -------- | --------------------------------------------------------------------------------------- |
| Props | value, inputId, inputClass, placeholder, disabledCountry, disabled, class |
| Events | onInput |
| Bindings | bind:value, bind:country, bind:valid, bind:element |
<InputPhone
inputId="buyer-phone"
inputClass="input-bordered input w-full"
bind:value={buyerPhone}
/>Switch
Use-case: binary flags in filters/settings.
| Type | Fields |
| -------- | ------------------------------------------- |
| Props | checked, styleType, class, disabled |
| Events | onchange |
| Bindings | bind:checked |
<Switch styleType="warning" bind:checked={isPrivate} />
<Switch
checked={telegramEnabledDraft}
onchange={(e: Event & { currentTarget: EventTarget & HTMLInputElement }) => {
telegramEnabledDraft = Boolean(e.currentTarget?.checked);
}}
/>Select
Use-case: selecting a value from options.
| Type | Fields |
| -------- | ---------------------------------------------------------------------------------------------------- |
| Props | value, options: Array<{ value; label }>, label, placeholder, disabled, required, class |
| Events | via standard <select> onchange |
| Bindings | bind:value |
<Select
label="Event"
bind:value={selectedEvent}
options={events.map((e) => ({ value: e._id, label: e.title }))}
placeholder="No event"
/>Other UI components
Button,BreadCrumbs,Loader,ImagesUploader,Editor,Popup,Alert,ThemeButton,Theme.
Minimal examples:
<Alert class="alert-info">Info message</Alert>
<Popup title="Hint">Popup text</Popup>
<ThemeButton />Stores / API / Utils / Directives (non-UI exports)
Stores
Exports: sessionStore, accountStore, sessionIsInited, accountStoreInited, deviceInfoStore, networkStore, geolocationStore, geolocationIsInited.
import {
sessionStore,
accountStore,
sessionIsInited,
accountStoreInited
} from '@rshval/svelte-components';
if (!$sessionIsInited) {
await sessionStore.initSession();
}
if (!$accountStoreInited) {
await accountStore.initAccount(API_BASE);
}API client
Exports: api.get/post/put/patch/del.
import { api, sessionStore } from '@rshval/svelte-components';
import { get } from 'svelte/store';
const session = get(sessionStore);
const res = await api.get(`${API_BASE}/events`, session?.dc1_auth_key);Storage helpers
Exports: storageGet, storageSet, storageRemove (typically stores a JSON string).
import { storageGet, storageSet, storageRemove } from '@rshval/svelte-components';
await storageSet('cart', JSON.stringify(items));
const saved = await storageGet('cart');
await storageRemove('cart');Utils
Exports: isObject, isValidEmail, patternPassword.
import { isObject, isValidEmail, patternPassword } from '@rshval/svelte-components';Directive
Export: clickOutside.
<script lang="ts">
import { clickOutside } from '@rshval/svelte-components';
let isOpen = $state(true);
</script>
<div use:clickOutside={() => (isOpen = false)}>...</div>Coverage of commonly used entities
UI
ModalButtonBreadCrumbsTableTableFiltersTablePaginationInputFieldInputPhoneSwitchSelectToastLoaderImagesUploaderEditorPopupAlertThemeButtonTheme
Non-UI
apisessionStore,sessionIsInitedaccountStore,accountStoreInitedstorageGet,storageSet,storageRemoveisObject,isValidEmail,patternPasswordclickOutsidedeviceInfoStorenetworkStoregeolocationStore,geolocationIsInited
Current vs legacy
- Current: APIs listed in the tables above (including
Modalwithbind:elementandToastwithitems). - Legacy/compatibility: old patterns where toast is passed as
{ type, message }should be mapped toitemson the consumer side.
Exports
Main export groups from src/lib/index.ts:
- Components:
Button,Badge,InputField,Textarea,Editor,Select,Loader,Modal,Switch,Alert,Popup,BreadCrumbs,Timer,Toast; - Complex components:
InputPhone,Notifications,Notification,Table,Theme,ThemeButton,Drawer; - Map:
Map,MapComponent,UiMap*,getGeolocation, mapbox event types; - Helpers:
clickOutside,blurOnEscape,isValid*,patternPassword,getColorByValue,isObject; - Plugins:
api,geoserviceApi,storageGet/storageSet/storageRemove; - Stores:
accountStore,sessionStore,networkStore,deviceInfoStore,geolocationStore,screenOrientationStore,noScrollAppStore.
ImagesUploader
ImagesUploader now validates files on the client before upload starts.
- Default supported formats:
image/png,image/jpeg(.png,.jpg,.jpeg). - Invalid files are rejected before preview/upload.
- Upload errors are returned through
onerror.
Props
accept?: string | string[]- accepted file formats (default:['image/png', 'image/jpeg']).maxFileSizeMb?: number- optional max file size in MB.validateFile?: (file: File) => string | null- custom validator, returns error text ornull.onerror?: (message: string, context?: { fileName?: string; code?: string }) => void- unified error callback for client/server upload errors.
Toast integration example
<script lang="ts">
import { ImagesUploader, Toast } from '@rshval/svelte-components';
let toastItems = $state([]);
function handleUploadError(message: string, context?: { fileName?: string }) {
toastItems = [
...toastItems,
{
type: 'error',
message: context?.fileName ? `${context.fileName}: ${message}` : message
}
];
}
</script>
<ImagesUploader
assetsGet="/api/assets"
assetsPost="/api/assets"
pathPrefix=""
onerror={handleUploadError}
/>
<Toast items={toastItems} />Development scripts
npm run dev
npm run check
npm run lint
npm run test
npm run build
npm run storybook
npm run build-storybookStorybook
For isolated visual component checks:
npm run storybookBuild static Storybook:
npm run build-storybookPublishing
Build package:
npm run prepackManual publish:
npm publish --access publicAutomated release via Changesets:
- Add a changeset (
npm run changeset) describing your changes. - On
main, workflow creates/updates a release PR with versions and changelog. - After merging the release PR, the package is published automatically (
npm run release).
Compatibility
| Package | Recommended version |
| ---------------------------------------------- | ------------------- |
| svelte (peer) | ^5.53.7 |
| @sveltejs/kit (optional, for SvelteKit apps) | ^2.53.4 |
| @tiptap/core and @tiptap/* (peer) | ^3.20.0 |
| @popperjs/core (runtime) | ^2.11.8 |
SSR limitations and notes
- Some components target browser-only environments (
mapbox-gl, geolocation, Capacitor plugins) and should be used on client side only. - For SSR SvelteKit pages, wrap browser-only components in
if (browser)checks or load them inonMount. - Map components require token setup and client-side initialization code.
- Capacitor plugins assume native environment access; in regular browsers API limitations and fallback behavior may apply.
Breaking changes policy
- Any removal/change of a public export (
exportsor named root export) is a breaking change and requires a major bump. - Changes to required component props and public store/helper API shape are also breaking changes.
- Consumers should pin major version (
^1.x) and read changelog before upgrading.
Detailed step-by-step extraction plan is in MIGRATION_TO_STANDALONE.md.
Migration checklist for extracting into a standalone repository
- Extract package into a separate git repository preserving history (
git subtree splitor history filtering). - Configure CI checks on pull requests:
check,prepack,build-storybook. - Enable changesets workflow for semver and changelog management.
- Verify public API: keep
exportscontracts stable and do not remove them without a major bump. - Add a consumer smoke example (minimal SvelteKit app) that installs package from tarball.
CI and smoke checks
Recommended CI checks before publishing:
npm run check
npm run prepack
npm run smoke:exports
npm run build-storybooksmoke:exports verifies that after prepack, all artifacts declared in exports are present in dist/.
QR scanner (event check-in, Web + Capacitor)
QrScanner gives a unified scanner API for browser and Capacitor apps.
- Web mode: uses native
BarcodeDetector, and falls back to@zxing/browser. - Capacitor mode: uses
@capacitor-mlkit/barcode-scanningif installed. - Built-in dedupe (
cooldownMs) prevents duplicate check-in actions for repeated scans. - Deep-link parser supports payload like
/board/tickets/scanner?ticket=.... - Visual scan feedback: green frame + check icon for success, red frame + cross icon for invalid scan.
<script lang="ts">
import { QrScanner, type ParsedQrPayload } from '@rshval/svelte-components';
type TicketStatus = 'idle' | 'loading' | 'valid' | 'used' | 'invalid';
let scannerRef: { pause: () => void; resume: () => void } | null = null;
let ticketNumber = '';
let status: TicketStatus = 'idle';
let message = 'Scan ticket QR to begin check-in';
async function loadTicketStatus(scanned: ParsedQrPayload) {
if (!scanned.ticketNumber) {
status = 'invalid';
message = 'QR does not contain a check-in ticket number';
return;
}
ticketNumber = scanned.ticketNumber;
status = 'loading';
scannerRef?.pause();
const result = await fetch(`/api/tickets/status?ticket=${ticketNumber}`).then((r) => r.json());
status = result.status;
message = result.message;
}
async function activateTicket() {
await fetch('/api/tickets/activate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ticketNumber })
});
status = 'used';
message = `Ticket ${ticketNumber} is activated`;
scannerRef?.resume();
}
</script>
<QrScanner
bind:this={scannerRef}
formats={['qr_code']}
cooldownMs={2500}
highlightFrame
showScanResult
scanResultDurationMs={1400}
isSuccessfulScan={(payload) => payload.kind === 'check-in-link' && Boolean(payload.ticketNumber)}
vibrateOnDetect
onDetect={loadTicketStatus}
onError={(error) => (message = error.message)}
/>
<p>{message}</p>
{#if status === 'valid'}
<button class="btn btn-success" onclick={activateTicket}>Activate ticket</button>
{/if}If you need lower-level control, use createQrScanner() directly:
import { createQrScanner } from '@rshval/svelte-components';
const scanner = createQrScanner({
cooldownMs: 2000,
formats: ['qr_code'],
onDetect: (payload) => console.log(payload.ticketNumber)
});
await scanner.start();QrScanner visual feedback props:
showScanResult(trueby default) — show status icon in the center after each detection.scanResultDurationMs(1400by default) — how long success/error state is displayed.isSuccessfulScan— custom predicate to mark a payload as successful; if returnsfalse, error state is shown.
