@juspay/svelte-ui-components
v4.33.3
Published
A themeable Svelte 5 UI component library with CSS custom property driven styling
Readme
@juspay/svelte-ui-components
A themeable Svelte 5 component library where every visual property is a CSS custom property. Build any design system on top — no source changes needed.
npm install @juspay/svelte-ui-componentsRequires svelte ^5.41.2 and type-decoder ^2.1.0 as peer dependencies.
Why This Library?
Most component libraries ship with a fixed look. Changing it means fighting overrides, patching internals, or forking.
This library takes a different approach: components are unstyled by default and expose every visual decision — colors, spacing, typography, borders, shadows, radii — as CSS custom properties. You define the design system. The components render it.
<!-- A button that looks however you want -->
<div class="my-theme">
<Button text="Continue" onclick={handleClick} />
</div>
<style>
.my-theme {
--button-color: #000;
--button-text-color: #fff;
--button-border-radius: 8px;
--button-padding: 12px 24px;
--button-font-size: 14px;
--button-font-weight: 600;
--button-hover-color: #222;
--button-border: 1px solid #333;
}
</style>Breaking changes, and the release they actually shipped in
Both changes below shipped in 3.5.1, which went out as a patch by mistake.
The release workflow picks the version from the newest commit alone, and the
commit that made the build green was a fix: — so the breaking commit beneath
it was never seen. 4.0.0 is that same content under the version number it
should have carried. A ^3.5.0 range already resolves to it; 3.5.0 is the
last release without these changes.
Every deprecated event-prop spelling is gone. The 190 camelCase and
mixed-case event props that 3.x accepted as aliases are now unknown props,
which Svelte drops without an error — the handler simply stops running.
npx sui-codemod ./src rewrites them, and
docs/MIGRATION_4.0.md lists every pair.
children is no longer a declared property on sui-chat-bubble,
sui-draggable and sui-resizable. Assigning it sets an inert expando and the
content silently disappears. Pass the content as light-DOM children instead,
which those wrappers forward through their default slot:
el.children = mySnippet; // before — no longer works<sui-draggable><div>…</div></sui-draggable>
<!-- after -->Reading element.children is what the change fixes. Declaring the property
replaced the inherited accessor, so up to 3.5.0 element.children returned
undefined on those three elements rather than an HTMLCollection, and
el.children.length threw. Removing the declaration gives the collection back.
You can find affected call sites with a dry run — this reports and writes nothing:
npx sui-codemod --dry-run ./srcIt flags .children = assignments only in files that also mention one of the
three elements, so ordinary DOM code is not reported. The fix moves content from
a JavaScript assignment into markup, which is a decision about where content is
authored, so the tool deliberately does not rewrite it for you. Requires Node
≥ 22.18; typescript is an optional peer dependency that the codemod's
Svelte-parsing paths need.
Nothing else in the custom-element surface breaks. Everything this release adds —
185 wrapper props, three previously unregistered elements, the spellcheck and
aria-haspopup mappings, the snippet props on Banner, ListItem, Menu, Modal and
Button, Accordion's disabled, LottiePlayer's callbacks — is new surface or a
fix to something that did not work.
Quick Start
<script lang="ts">
import { Button, Input, Toggle, Toast } from '@juspay/svelte-ui-components';
</script>
<!-- Basic button -->
<Button text="Submit" onclick={() => console.log('clicked')} />
<!-- Input with validation -->
<Input
value=""
placeholder="Enter email"
dataType="email"
onstatechange={(state) => console.log(state)}
/>
<!-- Toggle switch -->
<Toggle checked={false} text="Dark mode" onclick={(val) => console.log(val)} />Components
Inputs & Form Controls
| Component | Description | Docs |
| --------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------- |
| Button | Action trigger with circular loader, progress bar, icon/children snippets, and aria-expanded support. | docs |
| Input | Text field with built-in validation for email, phone, password, and custom patterns. Supports text transformers and textarea mode. | docs |
| InputButton | Input field fused with action buttons — for search bars, OTP entry, coupon codes. | docs |
| Select | Dropdown picker with single-select (with search), multi-select (checkboxes + Select All + Apply), and custom content slots. | docs |
| Toggle | Labeled on/off switch with sliding ball animation. | docs |
| Checkbox | Styled checkbox input with custom SVG checkmark. | docs |
| Radio | Styled radio button with custom circular indicator. | docs |
| Slider | Range slider with configurable min, max, step, and optional value display. | docs |
| Choicebox | Selectable option group with single-select (radio) or multi-select (checkbox) behavior and custom content. | docs |
| Label | A real <label> element — clicking the text focuses (and for a checkbox or radio, activates) the control named by for, with an optional accessible required marker. | docs |
| RatingGroup | Star rating input — a single role="slider" tab stop driven by arrow keys, Home/End or a click, with optional half stars and native form participation. | docs |
Display & Data
| Component | Description | Docs |
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------ |
| AnimatedNumber | Per-digit odometer that rolls a number to its new value. Takes a number formatted through Intl.NumberFormat, or an already-formatted string. | docs |
| Avatar | Circular avatar with image (Img with fallback) or text initial. | docs |
| Badge | Icon with a numeric/text badge overlay in the corner. | docs |
| GridItem | Grid cell with icon, label, and loading overlay animation. | docs |
| Icon | Clickable icon with optional text label. | docs |
| IconStack | Layered horizontal stack of overlapping circular icons/avatars. | docs |
| Img | Image with automatic fallback on load error. | docs |
| ListItem | Multi-section list row with images, labels, and accordion expansion. | docs |
| Pill | Compact label/tag for status or categories, optionally clickable with a11y. | docs |
| Status | Full-screen status display for success/failure screens. | docs |
| Table | Data table with keyed columns, built-in cell renderers, sorting, pagination and selection. | docs |
| RelativeTime | Auto-updating relative time display ("5 minutes ago") with locale support and optional tooltip. | docs |
| AspectRatio | Constrains content to a fixed width-to-height ratio using CSS aspect-ratio, reserving layout space before an image, video or iframe loads. | docs |
Feedback & Loading
| Component | Description | Docs | | --------------- | ----------------------------------------------------------------------------------------------- | --------------------------- | | Banner | Sticky notification banner with icon snippet, dismiss button, link text, and click interaction. | docs | | BrandLoader | Full-screen branded splash/loading animation. | docs | | Loader | Circular spinner with gradient foreground. | docs | | LoadingDots | Animated inline dot sequence with bounce/pulse animations. | docs | | Shimmer | Loading placeholder with animated shimmer effect. All visuals via CSS variables. | docs | | Toast | Animated slide-in notification with type variants and auto-dismiss. | docs | | Progress | Horizontal progress bar with animated fill. | docs | | Gauge | Semicircular gauge/meter with configurable segments and animated value. | docs |
Overlays & Panels
| Component | Description | Docs | | -------------------- | ------------------------------------------------------------------------------------------------------------- | -------------------------------- | | Modal | Dialog overlay with configurable size, alignment, header, footer, transitions, and back-press support. | docs | | ModalAnimation | Fly/fade transition wrapper for modal content. | docs | | OverlayAnimation | Fade transition wrapper for overlay backgrounds. | docs | | Sheet | Slide-in panel from any edge (left/right/top/bottom) with header, scrollable content, footer, and focus trap. | docs | | CommandMenu | Command palette (Ctrl+K) with fuzzy search, grouped commands, and keyboard navigation. | docs | | ContextMenu | Right-click context menu with nested submenus, separators, and keyboard navigation. | docs | | Menu | Dropdown action menu with keyboard navigation, typeahead, disabled/danger items, and separators. | docs | | Tooltip | Hover-triggered tooltip with configurable position and delay. | docs |
Navigation & Structure
| Component | Description | Docs |
| ----------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------- |
| Accordion | Expandable/collapsible container with CSS grid animation. | docs |
| Carousel | Horizontal content slider with swipe and pagination dots. | docs |
| CheckListItem | Checklist row with checkbox, label, and toggleable checked state. | docs |
| Pagination | Page navigation with previous/next buttons and numbered page links. | docs |
| Scroller | Overflowing item list with arrow navigation, gradient edges, drag-to-scroll, and snap support. | docs |
| Stepper | Multi-step progress indicator with completed, active, and pending states. | docs |
| Step | Individual step within a Stepper — renders number, label, and connector. | docs |
| Tabs | Tabbed interface with animated active indicator. | docs |
| Toolbar | Fixed header bar with back button, title, and customizable content areas. | docs |
| Separator | Horizontal or vertical divider — decorative and out of the accessibility tree by default, role="separator" when it demarcates meaningful sections. | docs |
Actions
| Component | Description | Docs | | ----------------- | ------------------------------------------------------------------------- | ----------------------------- | | Snippet | Copyable command-line snippet with prompt prefix and copy button. | docs | | SplitButton | Primary action button with dropdown secondary actions via Menu component. | docs | | KeyboardInput | Keyboard shortcut display with styled key caps. | docs | | ThemeSwitcher | Segmented control for light/dark/system theme switching. | docs |
Decorative
| Component | Description | Docs | | ----------- | ------------------------------------------------ | ----------------------- | | Book | 3D book display with animated page turning. | docs | | Browser | Browser window chrome frame for content display. | docs | | Phone | Smartphone device frame for app previews. | docs |
Web Components
Every component also ships as a real Custom Element (Svelte 5's native customElement
compiler output, shadow DOM), tagged sui-* — usable from React, Vue, Angular, or plain
HTML, no separate Svelte installation required; the runtime is bundled with each
component.
<!-- Always the latest release, via GitHub Pages -->
<script type="module" src="https://juspay.github.io/svelte-ui-components/wc/index.js"></script>
<!-- Pinned to an npm version, via jsDelivr or unpkg -->
<script
type="module"
src="https://cdn.jsdelivr.net/npm/@juspay/svelte-ui-components@latest/dist-wc/index.js"
></script>
<script
type="module"
src="https://unpkg.com/@juspay/svelte-ui-components@latest/dist-wc/index.js"
></script>
<sui-button>Click me</sui-button>
<sui-input placeholder="Type here"></sui-input>Or from npm directly, via the package's ./wc export:
import '@juspay/svelte-ui-components/wc';Theming works identically to the Svelte components — the same --{component}-{element}-{property}
CSS custom properties apply across the shadow DOM boundary.
Event handlers: use addEventListener, not element.onclick
Handlers are passed as props, and 93 of those props are named after event-handler
accessors that already exist on HTMLElement — onclick, onchange, oninput,
onkeydown, ontouchstart and 22 others. On a custom element the component's prop
wins, so assigning the property does not register a DOM handler the way it would
anywhere else:
const checkbox = document.querySelector('sui-checkbox');
// Sets the component's prop. On sui-checkbox and sui-toggle that prop is called
// with a boolean, so `event` here is `true` / `false`, not a MouseEvent.
checkbox.onclick = (event) => console.log(event.clientX); // undefined
// Always does what you expect.
checkbox.addEventListener('click', (event) => console.log(event.clientX));Two things are not affected: the inline HTML attribute (<sui-button onclick="...">)
never used this accessor, and addEventListener is untouched. Only the JavaScript
property assignment differs.
Renaming these is a breaking change to every element's JavaScript API, so the whole set
has to move at once at a major — the same way the aria* collisions were renamed in
4.0.0. Until then the existing declarations are recorded in
scripts/wc-parity/prop-parity.test.ts, which fails the build on a new one.
Theming
How It Works
Every component reads its visual properties from CSS custom properties with sensible defaults. To create a theme, define the variables on any ancestor element:
/* theme.css */
.my-design-system {
/* Button */
--button-color: #0070f3;
--button-text-color: #fff;
--button-border-radius: 6px;
--button-padding: 10px 20px;
--button-font-family: 'Inter', sans-serif;
--button-font-size: 14px;
--button-hover-color: #0060df;
/* Input */
--input-background: #fafafa;
--input-border: 1px solid #eaeaea;
--input-radius: 6px;
--input-font-family: 'Inter', sans-serif;
--input-focus-border: 1px solid #0070f3;
/* Toast */
--toast-border-radius: 8px;
--toast-font-family: 'Inter', sans-serif;
--toast-success-background-color: #0070f3;
/* Modal */
--modal-border-radius: 12px;
--modal-content-background-color: #fff;
--background-color: #00000066;
}<div class="my-design-system">
<Button text="Save" onclick={save} />
<Input value="" placeholder="Search..." />
</div>Dark Mode
The library ships a dark theme. Import it once and every component follows
data-theme="dark" on the document root — which is what ThemeSwitcher
already writes:
import '@juspay/svelte-ui-components/theme-dark.css';<script>
import { ThemeSwitcher } from '@juspay/svelte-ui-components';
</script>
<ThemeSwitcher
mode="segment"
onchange={(_, resolved) => {
document.documentElement.dataset.theme = resolved;
}}
/>The stylesheet only re-declares custom properties, so it composes with your own theme rather than replacing it: anything you set on a more specific ancestor still wins, and any variable you have not set keeps the library's dark value.
It also works for the <sui-*> custom elements. Custom properties inherit
across shadow boundaries, which is the only route into a shadow root from
outside — a selector-based override such as
[data-theme='dark'] .my-part { … } applies in the Svelte build and is dead
for the web component. Theme through variables and both work.
Scoped Theming
Because CSS variables cascade, you can scope different themes to different parts of your app:
<div class="light-theme">
<Button text="Light" onclick={handleClick} />
</div>
<div class="dark-theme">
<Button text="Dark" onclick={handleClick} />
</div>
<style>
.light-theme {
--button-color: #fff;
--button-text-color: #000;
--button-border: 1px solid #eaeaea;
}
.dark-theme {
--button-color: #111;
--button-text-color: #fff;
--button-border: 1px solid #333;
}
</style>Bridging Your Own Token Scale
The library names a property per component element (--{component}-{element}-{property}),
so components render correctly with no stylesheet at all. Most design systems
name the opposite thing: a short scale of values — a handful of neutrals, lines
and radii — composed everywhere.
Restating a scale as hundreds of per-component declarations, and keeping them in step by hand, is not the intended cost. Define your scale once and declare the component tokens in terms of it; the cascade does the rest, and a value changes in one place:
/* Your scale — the only values you maintain. */
:root {
--acme-surface-1: #ffffff;
--acme-surface-2: #f4f5f7;
--acme-line-2: #d6d9de;
--acme-ink-1: #1a1d21;
--acme-ink-2: #5b6371;
--acme-accent: #3b5bdb;
--acme-radius-md: 8px;
}
[data-theme='dark'] {
--acme-surface-1: #14161a;
--acme-surface-2: #1e2126;
--acme-line-2: #333942;
--acme-ink-1: #e6e8eb;
--acme-ink-2: #9aa3b2;
--acme-accent: #748ffc;
}
/* The bridge — component tokens expressed as your scale. Write it once. */
:root {
--button-color: var(--acme-accent);
--button-text-color: var(--acme-surface-1);
--button-border-radius: var(--acme-radius-md);
--button-secondary-text-color: var(--acme-ink-1);
--button-secondary-border-color: var(--acme-line-2);
--input-background: var(--acme-surface-1);
--input-text-color: var(--acme-ink-1);
--input-border: 1px solid var(--acme-line-2);
--input-placeholder-color: var(--acme-ink-2);
--table-header-background: var(--acme-surface-2);
--table-content-color: var(--acme-ink-1);
--table-border: 1px solid var(--acme-line-2);
}Note that a few tokens take a full shorthand rather than a bare colour —
--input-border and --table-border above are 1px solid …, not …. Setting
one to a colour alone produces an invalid declaration and no border at all,
which is silent. Each component's page in docs/ gives the shape it
expects.
Both themes now come from seven values. Because the bridge only re-declares
custom properties it composes with the shipped dark theme rather than fighting
it: import theme-dark.css for the components you have not bridged, and your
own declarations win wherever they are more specific.
The same file works for the <sui-*> elements, since custom properties are the
one thing that crosses a shadow boundary. A rule that reaches inside a
component — [data-theme='dark'] .my-part { … } — silently does nothing there.
CSS Variable Reference
Each component documents its full set of CSS variables in the docs/ directory. The variable naming convention follows the pattern:
--{component}-{element}-{property}For example:
--button-color— button background color--input-error-msg-text-color— input error message text color--modal-footer-primary-button-border-radius— border radius of the primary button inside a modal footer--toast-success-background-color— background color for success variant toasts
Props & Types
Components use a typed props pattern split into mandatory, optional, and event properties:
// Every component follows this pattern
type ButtonProperties = OptionalButtonProperties & ButtonEventProperties;
type OptionalButtonProperties = {
text?: string;
enable?: boolean;
disabled?: boolean;
showLoader?: boolean;
loaderType?: 'Circular' | 'ProgressBar';
type?: 'submit' | 'reset' | 'button';
icon?: Snippet;
children?: Snippet;
ariaLabel?: string;
ariaExpanded?: boolean;
// ...
};
type ButtonEventProperties = {
onclick?: (event: MouseEvent) => void;
onkeyup?: (event: KeyboardEvent) => void;
};All types are exported from the package:
import type {
ButtonProperties,
InputProperties,
ModalProperties,
SelectProperties,
ToastProperties,
MenuItem,
SheetSide
// ...
} from '@juspay/svelte-ui-components';Svelte 5 Patterns
This library is built on Svelte 5 and uses its modern APIs:
$props()for declaring component properties$state()and$derived()for reactive state$bindable()for two-way bound props (e.g.,Sheet.open,Banner.visible,Button.showProgressBar)Snippetfor composable content slots (e.g.,Button.icon,Sheet.content,Banner.rightContent)
<script lang="ts">
import { Sheet } from '@juspay/svelte-ui-components';
let open = $state(false);
</script>
<button onclick={() => (open = true)}>Open</button>
<Sheet bind:open title="Settings" side="right">
{#snippet content()}
<p>Sheet body goes here.</p>
{/snippet}
{#snippet footer()}
<button onclick={() => (open = false)}>Done</button>
{/snippet}
</Sheet>MCP Server
An MCP (Model Context Protocol) server ships as a separate package for AI-assisted development:
npm install @juspay/svelte-ui-components-mcpIt provides full component documentation — props, events, CSS variables, type references — through a standard MCP tool interface. Useful for integrating component knowledge into AI coding assistants.
Development
pnpm install # install dependencies
pnpm dev # start dev server with hot reload
pnpm build # build the library (vite + svelte-package + publint)
pnpm test # run integration + unit tests
pnpm lint # check formatting and lint rules
pnpm format # auto-format source filesProject Structure
src/lib/
{Component}/
{Component}.svelte # component implementation
properties.ts # prop type definitions
types.ts # shared types (ValidationState, InputDataType, etc.)
utils.ts # shared utilities (validateInput, prefersReducedMotion, body scroll lock)
index.ts # public exports
docs/ # component documentation (one markdown file per component)
mcp/ # MCP server package (separate npm package)Release
Pushing to the release branch triggers an automated CI pipeline:
- Lints the codebase
- Determines semver bump from conventional commit message (
feat:= minor,fix:= patch,!= major) - Bumps
package.jsonversion and generates a changelog - Builds the package
- Creates a GitHub release with a git tag
- Publishes to npm (
--access public)
License
MIT
