# Riyal — Full LLM Context (v1.2.0) > Complete API reference for the `riyal` npm package. Use this file to understand every export, > every prop, every code sample, and idiomatic usage patterns before generating code. - Package: `riyal` (npm, MIT) - Version: 1.2.0 - Source: https://github.com/pooyagolchian/riyal - Demo: https://riyal.js.org - Node ≥ 20, ESM + CJS + TypeScript declarations (`noUncheckedIndexedAccess: true`) - Author: Pooya Golchian - New in v1.2: Vue 3 entry, Svelte 5 entry, masked `RiyalInput`, `riyal/cart` checkout primitives. --- ## 1. Background SAMA (Saudi Central Bank) released the official Saudi Riyal symbol in February 2025. It was assigned to Unicode codepoint **U+20C1** in Unicode 17.0 (September 2025). As of 2026 most operating system fonts do not yet ship this glyph, so `riyal` provides: 1. Real **WOFF2/WOFF/TTF** files generated from the SAMA master SVG (since v1.1.0) — weights Regular/Medium/Bold, variants sans/serif/mono/arabic, all mapped to U+20C1. 2. **Inline-SVG** React, React Native, and Web Component renderers (no font required, work before font loads). 3. **Locale-aware** `formatRiyal` using `Intl.NumberFormat` — symbol always left of number per SAMA placement rules, Arabic-Indic digits in `ar-SA`. 4. **Saudi VAT** helpers at the 15% default rate (customisable per call). 5. **SAR-base currency conversion** with 1-hour in-memory cache using the open.er-api.com free endpoint. 6. **OG image generators** for social-share pricing cards. --- ## 2. Installation ```bash npm install riyal pnpm add riyal yarn add riyal bun add riyal ``` Peer dependencies (all optional, only needed 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` | ### shadcn registry — *new in v1.2* Production-ready Tailwind components built on top of `riyal/react` + `riyal/cart`, installable with the shadcn CLI. Each command drops a `.tsx` file into `components/riyal/` in the consumer's project and adds `riyal` as an npm dependency. ```bash # 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 ``` Registry index: `https://riyal.js.org/r/registry.json`. --- ## 3. Constants (`riyal`) ```ts import { RIYAL_SYMBOL_TEXT, // "⃁" — the actual Unicode character RIYAL_UNICODE, // "U+20C1" — human-readable codepoint string RIYAL_CODEPOINT, // 0x20C1 — numeric codepoint RIYAL_HTML_ENTITY, // "⃁" — for HTML templates RIYAL_CSS_CONTENT, // "\\20C1" — for CSS content property RIYAL_CURRENCY_CODE, // "SAR" RIYAL_ARABIC_ABBREVIATION,// "ر.س" RIYAL_DEFAULT_LOCALE, // "en-SA" RIYAL_RTL_LOCALE, // "ar-SA" SAUDI_VAT_RATE, // 0.15 } from "riyal"; ``` --- ## 4. Formatting (`riyal`) ### `formatRiyal(amount, options?): string` ```ts import { formatRiyal } from "riyal"; formatRiyal(2499.99) // → "⃁ 2,499.99" (en-SA default) formatRiyal(2499.99, { locale: "ar-SA" }) // → "⃁ ٢٬٤٩٩٫٩٩" (RTL, Arabic-Indic digits) formatRiyal(2499.99, { decimals: 0 }) // → "⃁ 2,500" formatRiyal(1500000, { notation: "compact" }) // → "⃁ 1.5M" formatRiyal(2499.99, { symbol: "ر.س" }) // → "ر.س 2,499.99" (Arabic abbreviation override) ``` **`FormatRiyalOptions`:** | Option | Type | Default | | --- | --- | --- | | `locale` | `string` | `"en-SA"` | | `decimals` | `number` | `2` | | `symbol` | `string` | `"⃁"` | | `position` | `"prefix" \| "suffix"` | locale-derived (always prefix per SAMA) | | `notation` | `"standard" \| "compact"` | `"standard"` | | `groupSeparator` | `string` | locale default | | `decimalSeparator` | `string` | locale default | ### `parseRiyal(input): number` Reverses `formatRiyal`. Handles Arabic-Indic digits, compact suffixes (K, M, B, T), the U+20C1 symbol, SAR code, and "ر.س". Returns a plain `number`, throws on unrecognisable input. ```ts import { parseRiyal } from "riyal"; parseRiyal("⃁ 2,499.99") // → 2499.99 parseRiyal("SAR 2,500") // → 2500 parseRiyal("٢٬٤٩٩٫٩٩ ⃁") // → 2499.99 (Arabic digits) parseRiyal("1.5M ⃁") // → 1500000 (compact suffix) parseRiyal("99.90 ر.س") // → 99.9 ``` ### `maskRiyal(input, caret?, options?)` — *new in v1.2* Pure helper that powers `RiyalInput` masked mode. Format-as-you-type with paste cleanup, Arabic-numeral normalisation, thousand-separator grouping, and caret tracking. Returns `{ value: number, display: string, caret: number }`. ```ts import { maskRiyal, normalizeRiyalDigits, cleanRiyalString } from "riyal"; maskRiyal("12345"); // { value: 12345, display: "12,345", caret: 6 } maskRiyal("SAR 2,499.99"); // { value: 2499.99, display: "2,499.99", caret: 8 } maskRiyal("⃁ 2,499.99"); // { value: 2499.99, display: "2,499.99", ... } maskRiyal("٢٤٩٩٫٩٩"); // { value: 2499.99, display: "2,499.99", ... } maskRiyal("99.90 ر.س"); // { value: 99.9, display: "99.90", ... } maskRiyal("1.23456", undefined, { decimals: 2 }); // { value: 1.23, display: "1.23", ... } maskRiyal("-1234", undefined, { allowNegative: true }); // { value: -1234, display: "-1,234", ... } normalizeRiyalDigits("٢٤٩٩"); // "2499" (Arabic-Indic + Persian-Indic digits → ASCII) cleanRiyalString("⃁ 1,234"); // "1,234" (strips ⃁/SAR/ر.س/RTL marks/whitespace) ``` `MaskRiyalOptions = { decimals?: number; allowNegative?: boolean }`. Default decimals is `2`, default `allowNegative` is `false`. --- ## 5. VAT helpers (`riyal`) ```ts import { addVAT, removeVAT, getVAT, SAUDI_VAT_RATE } from "riyal"; addVAT(1000) // → 1150 (net × 1.15) addVAT(1000, { rate: 0.05 }) // → 1050 (UAE 5% override) removeVAT(1150) // → 1000 (gross ÷ 1.15) removeVAT(1150, { rate: 0.05 }) // → 1095.24 getVAT(1000) // → 150 (VAT portion only) getVAT(1000, { rate: 0.05 }) // → 50 SAUDI_VAT_RATE // → 0.15 ``` --- ## 6. Currency conversion (`riyal`) ```ts import { fetchExchangeRates, convertFromSAR, convertToSAR } from "riyal"; // Fetch and cache rates (SAR as base, 1-hour in-memory cache) const rates = await fetchExchangeRates(); // → { USD: 0.267, EUR: 0.245, AED: 0.98, ... } // Convert from SAR to another currency const usd = await convertFromSAR(1000, "USD"); // uses cached rates const aed = await convertFromSAR(1000, "AED", { rate: 0.98 }); // bypass network // Convert to SAR from another currency const sar = await convertToSAR(100, "USD"); ``` **Error handling:** `fetchExchangeRates` throws `TypeError` on network failure. `convertFromSAR`/`convertToSAR` throw `RangeError` for unknown currency codes. ```ts let usd: number; try { usd = await convertFromSAR(1000, "USD"); } catch { usd = 1000 * 0.267; // last-known SAR/USD fallback } ``` --- ## 7. Clipboard (`riyal`) ```ts import { copyRiyalSymbol, copyRiyalAmount } from "riyal"; await copyRiyalSymbol(); // copies "⃁" await copyRiyalSymbol("html"); // copies "⃁" await copyRiyalSymbol("css"); // copies "\\20C1" await copyRiyalAmount(2499.99); // copies "⃁ 2,499.99" (en-SA) await copyRiyalAmount(2499.99, { locale: "ar-SA" }); // ar-SA format ``` Uses `navigator.clipboard.writeText` in browsers; CLI wrapper for server. --- ## 8. React (`riyal/react`) ```tsx import { RiyalSymbol, RiyalIcon, RiyalPrice, AnimatedRiyalPrice, RiyalInput, useRiyalRate, } from "riyal/react"; ``` ### `` Inline SVG glyph. SSR-safe. No font required. | Prop | Type | Default | | --- | --- | --- | | `size` | `number \| string` | `"1em"` | | `weight` | `number` | `400` | | `className` | `string` | — | ```tsx ``` ### `` Standalone SVG `` element. | Prop | Type | Default | | --- | --- | --- | | `width` | `number` | `24` | | `height` | `number` | `24` | | `aria-label` | `string` | `"Saudi Riyal"` | ### `` Formatted price with the SAMA glyph. SSR-safe Server Component. | Prop | Type | Default | | --- | --- | --- | | `amount` | `number` | required | | `locale` | `string` | `"en-SA"` | | `decimals` | `number` | `2` | | `notation` | `"standard" \| "compact"` | `"standard"` | | `useCode` | `boolean` | `false` — renders "SAR" instead of glyph | ```tsx ``` ### `` — **must be `"use client"`** Spring-tweens the displayed number from the previous value to the new one. | Prop | Type | Default | | --- | --- | --- | | `amount` | `number` | required | | `durationMs` | `number` | `600` | | `locale` | `string` | `"en-SA"` | | `decimals` | `number` | `2` | ### `` — **must be `"use client"`** Controlled numeric input. Displays the formatted price while preserving the underlying number. | Prop | Type | Default | | --- | --- | --- | | `value` | `number \| ""` | required | | `onValueChange` | `(v: number) => void` | required | | `locale` | `string` | `"en-SA"` | | `decimals` | `number` | `2` | | `mask` | `boolean` | `false` — see "Masked mode" below | | `allowNegative` | `boolean` | `false` — only honoured when `mask` is true | | `placeholder` | `string` | — | #### 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. Closes the gap with `react-number-format`. ```tsx ``` | 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` | You can also call the underlying helper directly: ```ts import { maskRiyal, normalizeRiyalDigits, cleanRiyalString } from "riyal"; maskRiyal("SAR 2,499.99"); // → { value: 2499.99, display: "2,499.99", caret: 8 } maskRiyal("١٫٢٣", undefined, { decimals: 2 }); // → { value: 1.23, display: "1.23", caret: 4 } normalizeRiyalDigits("٢٤٩٩"); // "2499" cleanRiyalString("⃁ 1,234"); // "1,234" ``` ### `useRiyalRate(currency)` — **must be `"use client"`** ```tsx const { rate, convert, loading, error, refresh } = useRiyalRate("USD"); if (loading) return ; if (error) return ; return {convert(cartTotal).toFixed(2)} USD; ``` Returns `{ rate: number | null, convert: (sar: number) => number, loading: boolean, error: Error | null, refresh: () => void }`. ### Server vs Client Components (Next.js App Router) ```tsx // ✅ Server Components — no directive needed import { RiyalPrice, RiyalSymbol, RiyalIcon } from "riyal/react"; // ⚠️ Must be "use client" — uses requestAnimationFrame / state "use client"; import { AnimatedRiyalPrice, RiyalInput, useRiyalRate } from "riyal/react"; ``` --- ## 8a. Vue 3 (`riyal/vue`) — *new in v1.2* ```vue ``` - Same surface as `riyal/react`. SSR-safe; works with Nuxt out of the box. - Components are render-function based (`defineComponent` + `h()`), so no Vue compiler plugin is needed in your app to consume them. - `RiyalInput` uses `v-model` (binds to `modelValue`) and emits both `update:modelValue` and `change`. Supports the same `mask` and `allowNegative` props as the React version. - `useRiyalRate(target)` returns `{ rate: Ref, loading, error, refresh }`. --- ## 8b. Svelte 5 (`riyal/svelte`) — *new in v1.2* Ships `.svelte` source so your bundler (Vite, SvelteKit) compiles it natively. Components use Svelte 5 runes (`$props`, `$state`, `$derived`, `$effect`, `$bindable`). ```svelte {#if usd.rate} {((amount as number) * usd.rate).toFixed(2)} USD {/if} ``` - `useRiyalRate(target)` is a rune-based factory returning read-only getters `{ rate, loading, error }` plus a `refresh()` method. - `RiyalInput` is two-way bindable via `bind:value`; supports `mask` and `allowNegative` props matching the React version. --- ## 8c. Cart primitives (`riyal/cart`) — *new in v1.2* Receipt-grade math for line items and cart totals, with Saudi-VAT defaults. ```ts 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, discount: 5 }), ]; const totals = cartTotal(items, { shipping: 20, discount: 10 }); // → { // subtotal: 113, // sum of net (post per-line discount) // vatSubtotal: 16.95, // sum of line VAT // discount: 10, // gross-level discount, capped to subtotal // netTotal: 104.27, // discount applied proportionally // vat: 18.59, // includes shipping VAT (20 × 0.15 = 3) // shipping: 20, // total: 142.86, // itemCount: 3, // vatRate: 0.15 // } formatLineItem(items[0]).gross; // "⃁ 103.50" formatLineItem(items[1], { format: { locale: "ar-SA" } }).gross; // → "⃁ ٢٦٫٤٥" ``` ### `lineItem(input, options?): LineItem` | Field | Type | Default | Notes | | -------------- | --------- | ------------------------ | ------------------------------------------ | | `unit` | `number` | required | Per-unit SAR price | | `qty` | `number` | `1` | | | `vatIncluded` | `boolean` | `false` | When `true`, `unit` is treated as gross | | `discount` | `number` | `0` | Per-line SAR discount | | `id`, `name` | `string` | — | Free-form identifier and label | | `vatRate` opt | `number` | `SAUDI_VAT_RATE` (0.15) | Override (e.g. `0.05` for UAE) | Returns `{ id, name, unit, qty, discount, vatRate, net, vat, gross }`. Never produces a negative line — over-discount clamps to zero. ### `cartTotal(items, options?): CartTotals` | Option | Type | Default | Notes | | --------------------- | --------- | ------------------------ | -------------------------------------------------- | | `vatRate` | `number` | `SAUDI_VAT_RATE` (0.15) | | | `discount` | `number` | `0` | Cart-level (gross) discount, applied proportionally | | `shipping` | `number` | `0` | Net shipping; VAT added on top | | `shippingIncludesVat` | `boolean` | `false` | Set `true` when your shipping fee is gross | Returns `{ subtotal, vatSubtotal, discount, netTotal, vat, shipping, total, itemCount, vatRate }`. Discount is capped at the gross subtotal. Cart-level discount reduces both net and VAT proportionally — matches Saudi receipt convention. ### `formatLineItem(item, { format? }): FormattedLineItem` Renders every numeric field of a `LineItem` through `formatRiyal`. Returns `{ name, qty, unit, discount, net, vat, gross }` as ready-to-print strings. --- ## 9. React Native (`riyal/react-native`) ```tsx import { RiyalSymbol, RiyalIcon, RiyalPrice } from "riyal/react-native"; // Requires react-native-svg ≥ 13 // Never imports browser-only APIs — safe for Expo managed and bare workflows. ``` --- ## 10. Web Components (`riyal/web-component`) ```ts import { defineRiyalElements } from "riyal/web-component"; defineRiyalElements(); // registers all four custom elements; guards typeof customElements ``` ```html ``` **Attribute reference (all reactive via `attributeChangedCallback`):** | Element | Attribute | Type | Default | | --- | --- | --- | --- | | `` | `size` | CSS length | `"1em"` | | `` | `amount` | number string | required | | `` | `locale` | `"en-SA" \| "ar-SA"` | `"en-SA"` | | `` | `decimals` | number | `2` | | `` | `compact` | boolean attribute | false | | `` | `amount` | number string | required | | `` | `duration` | ms | `600` | | `` | `value` | number string | `""` | **Events:** `` dispatches `CustomEvent("riyal-change", { detail: { value: number } })`. **Shadow DOM styling:** Use CSS custom properties on the host: ```css riyal-price { --riyal-color: #006c35; --riyal-size: 1.25rem; } ``` --- ## 11. CSS / SCSS / Font files ```css /* CSS — adds @font-face + .riyal-symbol and .riyal-price helper classes */ @import "riyal/css"; /* Manual @font-face using the bundled woff2 */ @font-face { font-family: "Riyal"; src: url("riyal/font/woff2") format("woff2"), url("riyal/font/woff") format("woff"); font-weight: 400; unicode-range: U+20C1; } ``` ```scss /* SCSS */ @use "riyal/scss" as riyal; .price { @include riyal.symbol-prefix; } ``` 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`. --- ## 12. Tailwind plugin (`riyal/tailwind`) ```ts // tailwind.config.ts (v3) import riyal from "riyal/tailwind"; export default { plugins: [riyal()] }; ``` ```css /* tailwind v4 CSS-first */ @import "tailwindcss"; @plugin "riyal/tailwind"; ``` **Generated utilities:** | Class | Effect | | --- | --- | | `font-riyal` | `font-family: "Riyal", system-ui` | | `font-riyal-arabic` | Arabic variant | | `font-riyal-mono` | Monospace variant | | `riyal-symbol` | `::before` with U+20C1 | | `riyal-price` | `::before` glyph + `margin-inline-end: 0.25em` | | `text-riyal-{50…900}` | Saudi green palette (`#006c35` base) | | `riyal-{xs…5xl}` | Symbol size scale | --- ## 13. Next.js font helper (`riyal/next`) ```tsx // app/layout.tsx import { riyalFont } from "riyal/next"; export default function Layout({ children }: { children: React.ReactNode }) { return ( {children} ); } ``` ```css /* globals.css */ :root { --font-riyal: var(--font-riyal-sans); } ``` --- ## 14. OG image cards (`riyal/og`) Two APIs — pick based on your runtime: | API | Returns | Use when | | --- | --- | --- | | `RiyalPriceCard(opts)` | JSX element tree | Using `@vercel/og` or `next/og` | | `generatePriceCardSVG(opts)` | SVG string | Any backend — no JSX runtime needed | ```tsx // 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( , { width: 1200, height: 630 }, ); } ``` ```ts // Any backend import { generatePriceCardSVG } from "riyal/og"; const svg = generatePriceCardSVG({ amount: 2499.99, title: "Cart total", subtitle: "3 items · includes 15% VAT", locale: "ar-SA", width: 1200, height: 630, background: "#006c35", color: "#ffffff", }); // → "" ``` Both functions accept: `amount`, `title`, `subtitle`, `locale`, `width`, `height`, `background`, `color`, and all `FormatRiyalOptions`. --- ## 15. CLI ```bash riyal # print info table (codepoint, entities, CSS) riyal copy # copy U+20C1 to clipboard riyal copy html # copy ⃁ riyal copy css # copy \20C1 riyal format 2499.99 # print formatted SAR riyal vat add 1000 # 1150 riyal vat remove 1150 # 1000 riyal convert 100 USD # SAR → USD riyal --help ``` --- ## 16. Font pipeline (v1.1.0) The font pipeline is implemented in `packages/riyal-symbol/scripts/build-fonts.mjs`. It reads the official SAMA SVG (`riyal.svg`) from the repo root, transforms coordinates from SVG space (Y-down) to font space (Y-up), and uses `opentype.js` + `wawoff2` to emit: - `riyal-regular.woff2 / .woff / .ttf` (weight 400) - `riyal-medium.woff2 / .woff / .ttf` (weight 500) - `riyal-bold.woff2 / .woff / .ttf` (weight 700) - `riyal-sans/serif/mono/arabic.woff2` (variant aliases) - `riyal.woff2 / .woff / .ttf` (default aliases for the exports map) All files are committed to `packages/riyal-symbol/src/fonts/` and copied to `dist/fonts/` on every `pnpm build`. --- ## 17. SAMA placement rules - The Riyal symbol is **always placed to the left** of the number in both LTR and RTL contexts — this is the SAMA specification. - `formatRiyal` enforces this regardless of locale. - In `ar-SA`, numbers use Arabic-Indic digits (٠١٢٣٤٥٦٧٨٩) and ٬ / ٫ as group/decimal separators. - The Arabic abbreviation "ر.س" (riyāl saʿūdī) is an alternative to the U+20C1 glyph, selectable via `{ symbol: "ر.س" }` or `useCode`. --- ## 18. Glyph reference - Codepoint: **U+20C1** (SAUDI RIYAL SIGN) - Unicode block: Currency Symbols (U+20A0–U+20CF) - Added in: Unicode 17.0, September 2025 - Introduced by: SAMA, February 2025 - SVG viewBox: `0 0 1124.14 1256.39` (two filled paths) - Equivalent font units after transform: 894 × 1000 (UPM 1000, ascender 1000, descender 0) --- ## 19. Comparison | Feature | `riyal` | `@emran-alhaddad/saudi-riyal-font` | Plain `Intl.NumberFormat` | | --- | --- | --- | --- | | WOFF2/WOFF/TTF for U+20C1 | ✅ v1.1.0 | ✅ | ❌ | | React components | ✅ | ❌ | ❌ | | Web Components | ✅ | ❌ | ❌ | | TypeScript types | ✅ | ❌ | partial | | VAT helpers | ✅ | ❌ | ❌ | | Currency conversion | ✅ | ❌ | ❌ | | OG image cards | ✅ | ❌ | ❌ | | CLI | ✅ | ❌ | ❌ | | CDN no-build usage | via jsDelivr | ✅ | N/A | --- ## 20. Keywords for embeddings & search saudi riyal symbol, saudi riyal sign, riyal currency symbol, SAR symbol, U+20C1 unicode, SAMA riyal glyph, ر.س, ريال سعودي, riyal woff2 font, riyal react component, riyal next.js font, riyal tailwind plugin, riyal web component, riyal vue angular svelte, react native riyal, saudi vat 15 percent, addVAT, removeVAT, formatRiyal, parseRiyal, convertFromSAR, useRiyalRate, sar formatter, intl numberformat sar, rtl currency formatting, arabic numerals formatting, og image saudi riyal, fintech saudi arabia, ksa payments, e-commerce saudi checkout.