A Vue component library and design system for rotki
You can start using the library after installing it from npm along with the Inter and Geist Mono fonts:
pnpm install -D --save-exact @rotki/ui-library @fontsource-variable/inter @fontsource-variable/geist-monoWith Tailwind CSS 4 in the app, import the library into your Tailwind entry (see Use the @rotki/ui-library Tailwind CSS theme) and the fonts in the project root (e.g. main.ts):
import '@fontsource-variable/inter/opsz.css';
import '@fontsource-variable/geist-mono';Without Tailwind, import the prebuilt stylesheet as well:
import '@rotki/ui-library/style.css';The library names the fonts but does not ship them. Inter's opsz.css adds the optical size axis, so large
headings get the display cut; @fontsource-variable/inter alone is about a third smaller. The static
@fontsource/inter and @fontsource/geist-mono builds work too. Self-host the fonts rather than loading
them from a CDN, so an offline app still renders them.
The package declares no side effects apart from its CSS, so your bundler ships only the components you
import. Importing the library does not set anything up globally. In particular, it registers the dayjs
utc, timezone and customParseFormat plugins only when RuiDateTimePicker is part of your bundle. If
your own code relies on those plugins, register them yourself.
To use the library you must install the library plugin:
import { createRui } from '@rotki/ui-library';
const RuiPlugin = createRui(options);
app.use(RuiPlugin);Then you can you the library components e.g.:
<script setup lang="ts">
import { RuiButton } from '@rotki/ui-library';
</script>
<template>
<div>
<RuiButton outlined>
This is button
</RuiButton>
</div>
</template>To dynamically manage the theme you can use the theme manager
import { useRotkiTheme } from '@rotki/ui-library';
const { toggleThemeMode, setThemeConfig, switchThemeScheme, state, store } = useRotkiTheme();
// to change the theme (pass colors as described by `ThemeConfig`) anytime:
setThemeConfig(newTheme);
// to switch between auto|light|dark
toggleThemeMode();
// to switch to a specific theme mode
switchThemeScheme(ThemeMode.dark);Icons must be registered when installing the RuiPlugin. There are two approaches:
The library provides a Vite plugin that automatically detects icon usage in your source files:
// vite.config.ts
import { ruiIconsPlugin } from '@rotki/ui-library/vite-plugin';
import vue from '@vitejs/plugin-vue';
import { defineConfig } from 'vite';
// eslint-disable-next-line import/no-default-export
export default defineConfig({
plugins: [
vue(),
ruiIconsPlugin({
// Optional: icons that are used dynamically and can't be statically detected
include: ['lu-dynamic-icon'],
// Optional: fail build on invalid icon names
strict: false,
// Optional: enable debug logging
debug: false,
}),
],
});Then import and use the detected icons:
// main.ts
import { createRui } from '@rotki/ui-library';
import icons from 'virtual:rotki-icons';
const RuiPlugin = createRui({
theme: { icons },
});
app.use(RuiPlugin);Add type support for the virtual module in your tsconfig.json:
{
"compilerOptions": {
"types": ["@rotki/ui-library/vite-plugin/client"]
}
}Handling Dynamic Icons:
The plugin can only detect icons that are statically analyzable. For dynamic icon names (e.g., computed from variables), you must manually include them:
<!-- These CAN be auto-detected: -->
<RuiIcon name="lu-star" />
<RuiIcon :name="'lu-check'" />
<!-- These CANNOT be auto-detected (use `include` option): -->
<RuiIcon :name="iconName" /> <!-- Variable reference -->
<RuiIcon :name="`lu-${direction}`" /> <!-- Template literal with variable -->// These CAN be auto-detected:
const icon = 'lu-arrow-down';
// These CANNOT be auto-detected (use `include` option):
const icons = props.items.map(i => i.icon); // Dynamic mappingYou can manually import and register specific icons:
import { createRui, LuArrowDown, LuCheck, LuStar } from '@rotki/ui-library';
const RuiPlugin = createRui({
theme: {
icons: [LuStar, LuCheck, LuArrowDown],
},
});
app.use(RuiPlugin);The library ships general-purpose icons only. Brand and logo icons (social, company, product marks) are intentionally not included — they are third-party trademarks with their own usage rules, so they belong in the application, not in the shared design system.
To use one, register its data yourself and tell the plugin the name is app-provided so it is not reported as an unknown icon:
// vite.config.ts — mark app-provided names as valid (not shipped by the library)
ruiIconsPlugin({ customIcons: ['lu-example-logo'] });// main.ts — supply the actual SVG data alongside the auto-detected icons
import icons from 'virtual:rotki-icons';
const exampleLogo = {
name: 'lu-example-logo',
components: [['path', { d: 'M…' }]], // one [tag, attrs] tuple per SVG primitive
};
const RuiPlugin = createRui({
theme: { icons: [...icons, exampleLogo] },
});Source brand logos from a dedicated, maintained set such as
Simple Icons, and follow each brand's usage
guidelines. <RuiIcon name="lu-example-logo" /> then renders as usual.
<script lang="ts" setup>
import { RuiIcon } from '@rotki/ui-library';
</script>
<template>
<div>
<RuiIcon name="lu-star" />
<RuiIcon name="lu-check" />
</div>
</template>The library makes no network requests. RuiLogo shows its bundled logo until the app tells it where a
named logo lives, through the logo.resolve option of createRui:
const RuiPlugin = createRui({
logo: {
// Return an image URL for a logo name, or undefined to keep the bundled logo
resolve: name => `/api/logo/${name}`,
},
});<RuiLogo logo="website" />The resolver runs in the browser after mount, never on the server, so server-rendered pages hydrate without mismatches. It may return a promise. Fetching, caching and validating the URL are up to the app, and a resolver that throws leaves the bundled logo in place.
To keep the seasonal logos that rotki publishes in rotki/data, the app looks up the mapping itself:
const branch = 'main';
const base = `https://raw.githubusercontent.com/rotki/data/${branch}`;
let mappings: Promise<Record<string, string> | undefined> | undefined;
async function loadMappings(): Promise<Record<string, string> | undefined> {
const response = await fetch(`${base}/constants/asset-mappings.json`);
return response.ok ? (await response.json()).logo : undefined;
}
const RuiPlugin = createRui({
logo: {
async resolve(name) {
mappings ??= loadMappings().catch(() => undefined);
const file = (await mappings)?.[name];
// Accept only a plain image filename, so the mapping cannot point anywhere else
if (!file || !/^[\w.-]+\.(?:png|svg|webp|gif|jpe?g)$/i.test(file))
return undefined;
return `${base}/assets/icons/${file}`;
},
},
});Keys are looked up as they appear in asset-mappings.json (for example empty_screen). uniqueKey still
appends key=<value> to the resolved URL, and src still bypasses the resolver.
The UI library supports internationalization through the createRuiI8nPlugin function. This allows you to provide translations for UI components.
First, you need to set up your Vue i18n instance and then connect it to the UI library:
import { createRui, createRuiI8nPlugin } from '@rotki/ui-library';
import { createApp } from 'vue';
import { createI18n } from 'vue-i18n';
// Create your i18n instance
const i18n = createI18n({
legacy: false, // You must set `false`, to use Composition API
locale: 'en',
messages: {
en: {
// Your app translations, then the UI library's own (see below)
rui: {
date_time_picker: {
date_after_max: 'Date cannot be after {date}',
date_before_min: 'Date cannot be before {date}',
date_in_future: 'The selected date cannot be in the future',
paste_unreadable: 'Could not read a date from the pasted text',
},
},
},
// Other languages...
},
});
const app = createApp(App);
// Create and use the Rotki UI plugin
const RuiPlugin = createRui();
app.use(RuiPlugin);
// Create and use the Rotki UI i18n plugin
const RuiI18nPlugin = createRuiI8nPlugin(i18n);
app.use(RuiI18nPlugin);
app.use(i18n);
app.mount('#app');The UI library uses the following translation keys:
// These are defined in src/i18n/keys.ts
export const RUI_I18N_KEYS = {
dateTimePicker: {
dateAfterMax: 'rui.date_time_picker.date_after_max',
dateBeforeMin: 'rui.date_time_picker.date_before_min',
dateInFuture: 'rui.date_time_picker.date_in_future',
pasteUnreadable: 'rui.date_time_picker.paste_unreadable',
},
} as const;The library is built for Tailwind CSS 4. Import it right after Tailwind in your CSS entry:
/* main.css */
@import 'tailwindcss';
@import '@rotki/ui-library/tailwind.css';That is the whole setup: there is no style.css to import. tailwind.css brings the theme, the color
values and the library's variants and utilities, and points your Tailwind build at the library's components,
so your build generates the classes they use, once, next to your own. A class you pass to a component then
competes with the component's own classes as any two Tailwind classes do, whatever order your stylesheets
load in.
@rotki/ui-library/style.css is the same library prebuilt with Tailwind itself, for an app without its own
Tailwind build. Don't import it alongside tailwind.css: everything would ship twice, and the prebuilt
classes would override your responsive ones (lg:grid-cols-3) by loading later.
The theme gives you the rui-* colors (bg-rui-primary, text-rui-text-secondary), the semantic surfaces and lines (bg-rui-surface, border-rui-divider, border-rui-outline), the role radii (rounded-rui-control, rounded-rui-card), the role shadows (shadow-rui-menu, shadow-rui-drawer, shadow-rui-tooltip, shadow-rui-control), the typography classes (text-body-1, text-h6) and a dark: variant that follows the theme useRotkiTheme sets. The tokens are CSS variables, so --radius-rui-control and the other role tokens can be retuned in your own CSS.
To install the dependencies you need to run on the root of the repository
pnpm install --frozen-lockfile
The following command when executed from the project root will build the @rotki/ui-library bundle.
This command will create the bundle for both Vue version >=3.4.3.
pnpm run build:prod
If you want to build for specific version, you can run:
pnpm run build
pnpm run lint
pnpm run lint:fix
pnpm run typecheck
In order to run the storybook, you can run:
pnpm run storybook
In order to test the components, you can run:
pnpm run test
In order to test the components in use in a vue 3 project, you can run:
pnpm run test:e2e
coverage results can be generated and previewed with:
pnpm run coverage
pnpm run coverage:preview
After you build the bundle, in the package.json on your main project, you can add this to the dependencies:
{
"@rotki/ui-library": "file:...path_of_this_directory"
}When the dependency installed on the main project, it will run the prepare script.
We use Lucide icons as the base icon set. Brand icons (Discord, Reddit, X/Twitter, etc.) in the src/custom-icons/ directory are sourced from Simple Icons.
The generator (scripts/generate-icons.ts) reads each Lucide icon's pre-parsed component data and the parsed XML of every SVG in src/custom-icons/, and emits one [tag, attrs] tuple per renderable primitive into src/icons/icons_*.ts. Multi-path icons stay as multiple entries — they are not collapsed into a single concatenated path. The generated files are gitignored and rebuilt by the build script; run the command below if you need to regenerate them by hand.
pnpm run generate-icons
AGPL-3.0 License © 2023- Rotki Solutions GmbH