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

riyal

v1.2.1

Published

The Saudi Riyal currency symbol (U+20C1) as a web font, CSS utility, React, Vue 3, Svelte 5, React Native, and Web Component, with a Tailwind plugin, Next.js font helper, OG cards, VAT helpers, and currency conversion.

Readme

riyal

npm version npm downloads license bundle

The Saudi Riyal currency symbol (U+20C1) toolkit — a web font, CSS, React, Vue 3, Svelte 5, React Native, Web Components, a Tailwind plugin, Next.js font helpers, OG image cards, and a CLI. Written in TypeScript, ships ESM + CJS + type defs.

Built around U+20C1 (Saudi Riyal Sign) — the codepoint scheduled for Unicode 17.0 (September 2025). Until OS fonts ship native support, this package renders the symbol via a bundled web font derived from the SAMA (Saudi Central Bank) glyph released in February 2025. Once Unicode 17.0 lands, the font becomes optional with no API changes.


Table of contents


Features

  • Tiny — tree-shakable ESM, ~58 kB packed (font included).
  • 🧮 Masked currency input — paste "SAR 2,499.99", "⃁ 2,499.99", or "٢٤٩٩٫٩٩" and get a clean number plus a perfectly formatted display.
  • 🎨 Multiple weights & families — sans, serif, mono, arabic.
  • 🧮 VAT helpers — Saudi 15% default, configurable.
  • 💱 Currency conversion — SAR-based, in-memory cached.
  • 🔤 Intl.NumberFormaten-SA and ar-SA (RTL) out of the box.
  • ⚛️ React, Vue 3, Svelte 5, React Native, and Web Components — pick your stack.
  • 🎯 Tailwind v3 & v4 plugin, Next.js next/font integration.
  • 🖼️ OG cards for share images.
  • 🛠️ CLI for quick lookups & copy-to-clipboard.

Framework matrix

| Stack | Entry | Components | | --- | --- | --- | | React 18+ | riyal/react | RiyalSymbol, RiyalIcon, RiyalPrice, AnimatedRiyalPrice, RiyalInput, useRiyalRate | | Vue 3.4+ | riyal/vue | Same surface as React, idiomatic Vue with defineComponent + composables | | Svelte 5 | riyal/svelte | Same surface as React, native .svelte components using runes | | React Native 0.72+ | riyal/react-native | RiyalSymbol, RiyalIcon, RiyalPrice | | Vanilla / Angular / Solid / others | riyal/web-component | <riyal-symbol>, <riyal-icon>, <riyal-price>, <riyal-animated-price>, <riyal-input> |


Installation

# pnpm
pnpm add riyal

# npm
npm install riyal

# yarn
yarn add riyal

# bun
bun add riyal

Peer deps (all optional, only required for the entry you import):

| Entry | Peer | | --- | --- | | riyal/react | react ≥ 18, react-dom ≥ 18 | | riyal/vue | vue ≥ 3.4 | | riyal/svelte | svelte ≥ 5 | | riyal/react-native | react-native ≥ 0.72, react-native-svg ≥ 13 | | riyal/tailwind | tailwindcss ≥ 3 | | riyal/next | next ≥ 13 |

Node ≥ 20 is required (for full ICU / Intl support).

Add via shadcn

Riyal ships a shadcn-compatible registry so you can pull production-ready components straight into your project with the shadcn CLI:

# Tailwind-styled SAR price tag (size + tone variants)
npx shadcn@latest add https://riyal.js.org/r/riyal-price-tag.json

# Form-grade SAR amount input (label, hint, error, masked editing)
npx shadcn@latest add https://riyal.js.org/r/riyal-amount-input.json

# Receipt-style cart summary (subtotal, VAT, shipping, grand total)
npx shadcn@latest add https://riyal.js.org/r/riyal-checkout-summary.json

Each item drops a .tsx file into components/riyal/ in your project so you own the source — and pulls riyal as an npm dependency for the underlying glyph, formatting, masking, and cart helpers.


Quick start

import { formatRiyal, addVAT, RIYAL_UNICODE } from "riyal";

formatRiyal(2499.99);
// → "ŝ 2,499.99"  (en-SA, U+20C1 + thin space)

formatRiyal(2499.99, { locale: "ar-SA" });
// → "٢٬٤٩٩٫٩٩ ŝ"  (ar-SA, RTL)

addVAT(100); // 115     (15% Saudi VAT)
RIYAL_UNICODE; // "ŝ"     (U+20C1)

Core API

Imported from the root entry: import { ... } from "riyal";

formatRiyal(amount, options?)

Format a number as a Riyal-prefixed/suffixed string using Intl.NumberFormat.

import { formatRiyal } from "riyal";

formatRiyal(1234.5);
formatRiyal(1234.5, {
  locale: "ar-SA",
  decimals: 0,
  symbol: "ر.س", // override the glyph
  position: "suffix", // "prefix" | "suffix"
  compact: true, // 1.2K-style notation
});

FormatRiyalOptions:

| Option | Type | Default | | ------------------------------------- | ---------------------- | ---------------- | | locale | string | "en-SA" | | decimals | number | 2 | | symbol | string | "\u20C1" | | position | "prefix" \| "suffix" | locale-dependent | | compact | boolean | false | | groupSeparator / decimalSeparator | string | locale defaults |

parseRiyal(input)

Strict reverse of formatRiyal — supports compact (1.2K), grouped, and Arabic-Indic digits.

parseRiyal("ŝ 2,499.99"); // 2499.99
parseRiyal("١٬٢٣٤٫٥٠ ŝ"); // 1234.5
parseRiyal("1.2K"); // 1200

VAT helpers

import { addVAT, removeVAT, getVAT, SAUDI_VAT_RATE } from "riyal";

addVAT(100); // 115
removeVAT(115); // 100
getVAT(100); // 15
addVAT(100, { rate: 0.05 }); // 105
SAUDI_VAT_RATE; // 0.15

Cart & checkout primitives — riyal/cart

Receipt-grade math for line items and cart totals, with Saudi-VAT defaults.

import { lineItem, cartTotal, formatLineItem } from "riyal/cart";

const items = [
  lineItem({ name: "Coffee Mug", unit: 45, qty: 2 }),
  lineItem({ name: "Filter Pack", unit: 28, qty: 1 }),
];

const totals = cartTotal(items, { shipping: 20, discount: 10 });
// → {
//   subtotal: 118,        // sum of net
//   vatSubtotal: 17.7,    // 15% of subtotal
//   discount: 10,         // capped to grossSubtotal
//   netTotal: 109.13,     // discount applied proportionally
//   vat: 19.36,           // includes shipping VAT
//   shipping: 20,
//   total: 148.49,
//   itemCount: 3,
//   vatRate: 0.15
// }

formatLineItem(items[0]).gross; // → "⃁ 103.50"

Highlights:

  • lineItem({ unit, qty, vatIncluded?, discount? }, { vatRate? }) — handles both VAT-net (default) and VAT-inclusive catalogue prices, plus per-line discount, and never produces a negative line.
  • cartTotal(items, { discount?, shipping?, shippingIncludesVat?, vatRate? }) — applies the cart-level discount proportionally to net + VAT (Saudi receipt convention), adds shipping with VAT-on-top by default, and caps discounts at the gross subtotal.
  • formatLineItem(item, { format? }) — renders every numeric field through formatRiyal for receipts, OG cards, and table rendering.

Currency conversion (SAR base)

import { fetchExchangeRates, convertFromSAR, convertToSAR } from "riyal";

const rates = await fetchExchangeRates(); // cached 1h in-memory
await convertFromSAR(1000, "USD"); // SAR → USD
await convertToSAR(100, "USD"); // USD → SAR
await convertFromSAR(100, "USD", { rate: 0.27 }); // bypass network

Clipboard

import { copyRiyal } from "riyal";

await copyRiyal(); // "\u20C1"
await copyRiyal({ format: "html" }); // "&#x20C1;"
await copyRiyal({ format: "css" }); // "\\20C1"

Error handling

fetchExchangeRates throws a TypeError when the network is unavailable. convertFromSAR / convertToSAR throw a RangeError when the target currency is not in the rate table. Always wrap in try/catch in production:

import { convertFromSAR } from "riyal";

let usd: number;
try {
  usd = await convertFromSAR(1000, "USD");
} catch {
  usd = 1000 * 0.267; // last-known SAR/USD fallback
}

useRiyalRate surfaces the error in its return value — no extra try/catch needed:

const { convert, loading, error } = useRiyalRate("EUR");

if (error) return <span>Rates unavailable</span>;
if (loading) return <span>Loading…</span>;
return <span>{convert(cartTotal)} EUR</span>;

React

pnpm add riyal react react-dom
import "riyal/css";
import {
  RiyalSymbol,
  RiyalIcon,
  RiyalPrice,
  AnimatedRiyalPrice,
  RiyalInput,
  useRiyalRate,
} from "riyal/react";

<RiyalSymbol />

Inline span using the bundled font — sized via CSS em.

<RiyalSymbol size={24} />
<RiyalSymbol size="1.25em" weight={600} />

<RiyalIcon />

Standalone SVG icon (no font required).

<RiyalIcon width={32} height={32} aria-label="SAR" />

<RiyalPrice />

Formatted price, locale-aware.

<RiyalPrice amount={2499.99} />
<RiyalPrice amount={2499.99} locale="ar-SA" decimals={0} />
<RiyalPrice amount={1_200_000} compact />

<AnimatedRiyalPrice />

Spring-animated counter for live totals.

<AnimatedRiyalPrice amount={cartTotal} duration={400} />

<RiyalInput />

Controlled numeric input that displays formatRiyal while preserving the underlying number.

const [value, setValue] = useState<number | "">(0);

<RiyalInput value={value} onValueChange={setValue} locale="ar-SA" />;

Masked mode

Pass mask to switch the input into a format-as-you-type field with paste cleanup, Arabic-numeral normalisation, thousand-separator grouping, and caret preservation:

<RiyalInput mask value={value} onValueChange={setValue} />;

| User does | Input shows | onValueChange receives | | --- | --- | --- | | Types 1234 | "1,234" | 1234 | | Pastes "SAR 2,499.99" | "2,499.99" | 2499.99 | | Pastes "⃁ 2,499.99" | "2,499.99" | 2499.99 | | Pastes "٢٤٩٩٫٩٩" | "2,499.99" | 2499.99 | | Pastes "99.90 ر.س" | "99.90" | 99.9 |

Add allowNegative to permit a leading -. The same mask and allowNegative props are available on the Vue and Svelte versions of RiyalInput.

You can also call the underlying helper directly:

import { maskRiyal, normalizeRiyalDigits } from "riyal";

const r = maskRiyal("SAR 2,499.99");
// → { value: 2499.99, display: "2,499.99", caret: 8 }

normalizeRiyalDigits("٢٤٩٩"); // "2499"

useRiyalRate(target)

Tiny hook around convertFromSAR. Caches per target, refreshes hourly.

const { rate, convert, loading, error } = useRiyalRate("USD");

return <span>{convert(2499.99)} USD</span>;

Vue 3

pnpm add riyal vue
<script setup lang="ts">
import { ref } from "vue";
import { RiyalPrice, RiyalInput, useRiyalRate } from "riyal/vue";

const amount = ref<number | "">(2499.99);
const usd = useRiyalRate("USD");
</script>

<template>
  <RiyalPrice :amount="2499.99" locale="ar-SA" />
  <RiyalInput v-model="amount" mask />
  <span v-if="usd.rate.value">{{ (Number(amount) * usd.rate.value).toFixed(2) }} USD</span>
</template>

The Vue entry exposes the same surface as riyal/reactRiyalSymbol, RiyalIcon, RiyalPrice, AnimatedRiyalPrice, RiyalInput (with mask and allowNegative props), and the useRiyalRate composable. SSR-safe; works with Nuxt out of the box.

RiyalInput uses v-model (binds to modelValue) and emits both update:modelValue and change.


Svelte 5

pnpm add riyal svelte
<script lang="ts">
  import {
    RiyalPrice,
    RiyalInput,
    useRiyalRate,
  } from "riyal/svelte";

  let amount: number | "" = $state(2499.99);
  const usd = useRiyalRate("USD");
</script>

<RiyalPrice amount={2499.99} locale="ar-SA" />
<RiyalInput bind:value={amount} mask />
{#if usd.rate}
  <span>{((amount as number) * usd.rate).toFixed(2)} USD</span>
{/if}

The Svelte entry ships .svelte source so your bundler (Vite, SvelteKit) compiles it natively. Components use Svelte 5 runes ($props, $state, $derived, $effect, $bindable); the useRiyalRate composable is a rune-based factory that returns read-only getters plus a refresh() method.


Web Components

Framework-agnostic — works with Vue, Angular, Svelte, Solid, and vanilla HTML. Registers <riyal-symbol>, <riyal-icon>, and <riyal-price>.

import { defineRiyalElements } from "riyal/web-component";
import "riyal/css";

defineRiyalElements();
<riyal-symbol size="1.25em"></riyal-symbol>
<riyal-icon width="24" height="24"></riyal-icon>
<riyal-price amount="2499.99" locale="ar-SA" compact></riyal-price>

Attribute reference

| Element | Attribute | Type | Default | Reactive | | --- | --- | --- | --- | --- | | <riyal-symbol> | size | CSS length | 1em | yes | | <riyal-icon> | width / height | number (px) | 24 | yes | | <riyal-icon> | aria-label | string | "Saudi Riyal" | yes | | <riyal-price> | amount | number string | required | yes | | <riyal-price> | locale | "en-SA" | "ar-SA" | "en-SA" | yes | | <riyal-price> | decimals | number | 2 | yes | | <riyal-price> | compact | boolean attribute | false | yes | | <riyal-animated-price> | amount | number string | required | yes | | <riyal-animated-price> | duration | number (ms) | 600 | yes | | <riyal-input> | value | number string | "" | yes |

All attributes are observed — setting them via setAttribute or a framework binding triggers a re-render with no extra boilerplate.

Events

<riyal-input> dispatches a riyal-change CustomEvent when the value changes:

document.querySelector("riyal-input").addEventListener("riyal-change", (e) => {
  console.log(e.detail.value); // number
});

Shadow DOM styling

Each element uses a closed shadow root. Override the symbol color and size with CSS custom properties exposed on the host:

riyal-price {
  --riyal-color: #006c35; /* Saudi green */
  --riyal-size: 1.25rem;
}

React Native

import { RiyalSymbol, RiyalPrice } from "riyal/react-native";

// Renders via react-native-svg — no font installation required.
<RiyalPrice amount={2499.99} />;

CSS / SCSS

Self-host the font + utility classes:

/* CSS */
@import "riyal/css";
// SCSS
@use "riyal/scss" as riyal;

Or pull a single weight:

@font-face {
  font-family: "Riyal";
  src: url("riyal/font/woff2") format("woff2");
}

Available font subpaths:

  • riyal/font/woff2
  • riyal/font/woff
  • riyal/font/ttf
  • riyal/font/sans/woff2
  • riyal/font/serif/woff2
  • riyal/font/mono/woff2
  • riyal/font/arabic/woff2

Tailwind plugin

Works with Tailwind v3 and v4.

// tailwind.config.ts
import riyal from "riyal/tailwind";

export default {
  plugins: [riyal()],
};

Adds these utilities:

| Class | Effect | | --- | --- | | font-riyal | font-family: "Riyal", system-ui | | font-riyal-arabic | Arabic variant of the Riyal font | | font-riyal-mono | Monospace variant | | riyal-symbol | ::before with U+20C1 glyph | | riyal-price | ::before glyph + margin-inline-end: 0.25em | | text-riyal-{50…900} | Saudi green palette (#006c35 base) | | riyal-{xs,sm,base,lg,xl,2xl} | Symbol size utilities |

<span class="font-riyal text-riyal-700">2,499.99</span>
<span class="riyal-symbol text-riyal-500 riyal-lg"></span>

Tailwind v4 — use the CSS-first config:

/* app.css */
@import "tailwindcss";
@plugin "riyal/tailwind";

Next.js font helper

// app/layout.tsx
import { riyalFont } from "riyal/next";

export default function Layout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={riyalFont.variable}>
      <body>{children}</body>
    </html>
  );
}
/* globals.css */
:root {
  --font-riyal: var(--font-riyal-sans);
}

Server vs Client Components (Next.js App Router)

RiyalPrice and RiyalSymbol have no client-side state — use them directly in Server Components. AnimatedRiyalPrice and RiyalInput require requestAnimationFrame / React state, so they must be Client Components:

// app/product/page.tsx — Server Component, no directive needed
import { RiyalPrice } from "riyal/react";

export default function ProductPage() {
  return <RiyalPrice amount={2499.99} />;
}
// components/cart-total.tsx — must be a Client Component
"use client";
import { AnimatedRiyalPrice } from "riyal/react";

export function CartTotal({ total }: { total: number }) {
  return <AnimatedRiyalPrice amount={total} duration={400} />;
}

OG image cards

Generate Open Graph share images on the fly. Two APIs — pick one:

| API | Use when | | --- | --- | | RiyalPriceCard(opts) | You're using @vercel/og or next/og — returns a JSX element tree | | generatePriceCardSVG(opts) | Any backend / serverless function — returns an SVG string, no JSX runtime needed |

With @vercel/og or Next.js App Router:

// app/og/route.tsx
import { ImageResponse } from "next/og";
import { RiyalPriceCard } from "riyal/og";

export const runtime = "edge";

export function GET() {
  return new ImageResponse(
    <RiyalPriceCard amount={2499.99} title="iPhone 16 Pro" locale="ar-SA" />,
    { width: 1200, height: 630 }
  );
}

With any backend (returns SVG string):

import { generatePriceCardSVG } from "riyal/og";

const svg = generatePriceCardSVG({
  amount: 2499.99,
  title: "Cart total",
  subtitle: "3 items",
  locale: "ar-SA",
  width: 1200,
  height: 630,
  background: "#006c35",
  color: "#ffffff",
});
// → <svg xmlns="http://www.w3.org/2000/svg" ...>…</svg>

Both functions accept the same options: amount, title, subtitle, locale, width, height, background, color, and all FormatRiyalOptions.


CLI

Installed automatically as a riyal bin.

riyal symbol             # prints U+20C1
riyal copy               # copies the glyph to clipboard
riyal format 2499.99     # formatted SAR
riyal vat add 100        # 115
riyal convert 100 USD    # SAR → USD
riyal --help

Constants & locales

import {
  RIYAL_UNICODE, // "\u20C1"
  RIYAL_CODEPOINT, // 0x20C1
  RIYAL_HTML_ENTITY, // "&#x20C1;"
  RIYAL_CSS_CONTENT, // "\\20C1"
  RIYAL_CURRENCY_CODE, // "SAR"
  RIYAL_ARABIC_ABBREVIATION, // "ر.س"
  RIYAL_DEFAULT_LOCALE, // "en-SA"
  RIYAL_RTL_LOCALE, // "ar-SA"
} from "riyal";

Browser support

  • Modern evergreen browsers (Chrome, Edge, Firefox, Safari).
  • Safari ≥ 16, Chrome ≥ 110, Firefox ≥ 110 (uses Intl.NumberFormat with notation: "compact").
  • Node ≥ 20 for non-browser usage.

The bundled font ships as WOFF2 (preferred), WOFF, and TTF.


Why riyal?

vs plain Intl.NumberFormat

Intl.NumberFormat formats numbers but does not know about U+20C1 — you'd still need to append the symbol manually, handle RTL placement, and build VAT and conversion helpers yourself. riyal wraps all of that in one package.

What riyal includes

| Feature | riyal | | --- | --- | | Web font (WOFF2/WOFF/TTF) | yes | | U+20C1 + U+E900 (legacy) | yes | | formatRiyal / parseRiyal | yes | | VAT helpers | yes | | Currency conversion | yes | | React components | yes | | Web Components | yes | | TypeScript types | yes | | CDN / no-build usage | via jsDelivr |


Contributing

PRs welcome. See CONTRIBUTING.md and the Code of Conduct.

pnpm install
pnpm --filter riyal dev    # watch builds
pnpm test                  # vitest
pnpm lint && pnpm format   # biome

Releases ship via Changesets:

pnpm changeset

License

MIT © Pooya Golchian

The Saudi Riyal symbol glyph is based on the SAMA (Saudi Central Bank) design released in February 2025 and mapped to U+20C1.