Documentation
What you need to know before the first component renders: how to install a private package, what the provider owns, and the three behaviours that are decided at the library level rather than in your app.
Installation
The package lives on a private npm scope, so npm needs a read-only token before it can see it. Put the token in .npmrc — locally and in CI — and the install is ordinary from there. React 18 and 19 are both supported; the icon set is a peer dependency so your app controls its version.
# .npmrc — locally and in CI
@nili:registry=https://registry.npmjs.org/
//registry.npmjs.org/:_authToken=${NILI_TOKEN}
npm i @nili/ui @remixicon/reactThe provider
NiliProvider owns exactly two global decisions: which theme is painted and which way the page reads. It deliberately does not own translation — components take their strings as props, so the library ships without an i18n runtime and you stay free to use i18next, react-intl, Next.js routing, or nothing at all.
import { NiliProvider } from '@nili/ui';
import '@nili/ui/styles.css';
export default function App({ children }) {
return (
<NiliProvider locale="fa" defaultTheme="system">
{children}
</NiliProvider>
);
}Direction
There is no direction prop, and nothing reads document.dir while rendering. Components are written with logical properties, so direction is inherited from CSS — which is why a right-to-left subtree works inside a left-to-right page, and why server rendering produces the correct markup on the first pass.
// Every component is written this way — logical, not physical.
<div className="ms-4 ps-2 border-s">…</div>
// So a right-to-left island inside a left-to-right page just works,
// with no prop threaded down to make it happen.
<section dir="rtl" lang="fa">
<Button endIcon={<RiArrowRightLine />}>ادامه</Button>
</section>Numerals
Digits are formatted at the value layer, through Intl, never by a font that draws local glyphs on ASCII codepoints. That distinction is what lets find-in-page, copy-paste and screen readers agree with the screen — and what leaves an escape hatch for values that must stay Latin, like an IBAN or an order number.
import { formatNumber, parseLocalizedNumber } from '@nili/ui';
formatNumber(1234567, { locale: 'fa-IR' }); // ۱٬۲۳۴٬۵۶۷
formatNumber(1234567, { locale: 'ar-EG' }); // ١٬٢٣٤٬٥٦٧
parseLocalizedNumber('۱٬۲۳۴٬۵۶۷'); // 1234567
// And the escape hatch, for a value that must stay Latin:
<TextInput label="IBAN" latin defaultValue="IR82 0540 …" />Tokens and theming
Components never write a dark: variant. Each names one semantic token and the value swaps underneath it, keyed off a data-theme attribute rather than a class. An attribute is open-ended, so a new theme — high contrast, a tenant brand — is a stylesheet rather than a refactor.
--nili-blue-500: #335CFF; /* primitive */
--nili-primary-base: …; /* semantic */
--color-primary-base: …; /* @theme */
class="bg-primary-base" /* component */
/* Dark mode is an attribute, not a class — so a third theme
is [data-theme='high-contrast'] and nothing else changes. */
<html data-theme="dark">Accessibility
Anything that has no visible label asks for one in its types rather than accepting a nameless control — an icon-only button requires aria-label, a dismiss button requires dismissLabel. The rule is that the type system, not a review comment, is what catches an unlabelled control.
// The type system refuses an unlabelled control.
<Button iconOnly aria-label="Close" startIcon={<RiCloseLine />} />
<Tag onDismiss={remove} dismissLabel="Remove tag">Design</Tag>
<Alert title="Saved" urgency="polite" />Then browse the components — each opens on the overview Storybook shows, with the source beside it. Components