Skip to content

About

Highly customisable Svelte UI Components

Resources

Stars

10 stars

Watchers

4 watching

Forks

Latest commit

 

History

913 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@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-components

Requires 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 ./src

It 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

Charts

Pure-SVG charts with no charting-library dependency; each takes plain data arrays.

Component Description Docs
AreaChart Responsive area chart for volume under a trend: multiple series, regular or 100% stacking, gradient fill, data labels and a hover overlay. docs
BarChart Responsive bar chart for comparing categories: vertical or horizontal, single or multi-series (grouped or stacked), value labels, per-point colours and hover tooltips. docs
DualAxisBarChart Two independent Y-axes over one categorical X-axis; each series declares its own axis and render type. docs
FunnelChart Horizontal funnel: one bar per stage, proportional to its value, with connectors between stages. docs
LineChart Responsive line chart for trends: multiple series, five curve interpolations, gradient fill and an optional area mode. docs
PieChart Responsive pie or donut chart with slice hover highlighting, a delta badge, custom labels, a legend and donut centre content. docs
SankeyChart Sankey diagram for flows between nodes, positioned into columns automatically. 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)
  • Snippet for 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-mcp

It 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 files

Project 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:

  1. Lints the codebase
  2. Determines semver bump from conventional commit message (feat: = minor, fix: = patch, ! = major)
  3. Bumps package.json version and generates a changelog
  4. Builds the package
  5. Creates a GitHub release with a git tag
  6. Publishes to npm (--access public)

License

MIT

About

Highly customisable Svelte UI Components

Resources

Stars

10 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages