# Oyna UI: the whole documentation
> A Vue 3 component library with a dark glass look. Plain CSS, no Tailwind.
# Why Oyna UI
I was building [invoke.wtf](https://invoke.wtf), a trainer for Invoker from Dota 2, and wanted a UI kit that looked the way I had in mind: dark glass over a rich background, no grey borders, light instead of lines, big condensed numbers, everything reachable from the keyboard.
I did not find one. Most Vue UI kits look like admin panels: white cards, grey borders, the same blue button. So I drew the interface by hand.
Oyna UI is that look taken out of the project and made into a library: the same surfaces, rings, type and hotkeys, without anything about the game. I made it for myself. If it suits your project, use it too.
## What is inside
Plain CSS and Vue. There is no Tailwind, no UnoCSS and no CSS-in-JS: you import one stylesheet and use the components, with nothing to configure. I did not want a library that makes you adopt a styling tool to get a button.
The one dependency is [Reka UI](https://reka-ui.com). Focus traps, keyboard navigation and screen reader support are hard to get right, and it already gets them right, so accessibility is not reinvented here. Everything you see is Oyna UI; Reka UI works underneath, where there is nothing to look at.
## What it is for
Interfaces that want character: dashboards, tools, landing pages, side projects. It is one look, done on purpose, not a neutral base to build any design on. You can change the accent, the radius and the fonts; you cannot make it look like a white admin panel, and it has no light theme.
## Where to see it
- [invoke.wtf](https://invoke.wtf) is where the look was born. The site itself is written by hand: the library came out of it, not the other way round.
- The [dashboard](https://oyna-ui.org/examples/dashboard) and [settings](https://oyna-ui.org/examples/settings) examples are built only from the library's components.
## The name
_Oyna_ is Uzbek for "glass", and also for "window" and "mirror".
Source: https://oyna-ui.org/guide/why
---
# Installation
## Install
```bash [npm]
npm install oyna-ui
```
```bash [pnpm]
pnpm add oyna-ui
```
```bash [yarn]
yarn add oyna-ui
```
```bash [bun]
bun add oyna-ui
```
`vue` 3.5 or newer is a peer dependency.
## Register
Import the stylesheet once and register the components:
```ts
import oyna from 'oyna-ui'
import { createApp } from 'vue'
import App from './App.vue'
import 'oyna-ui/style.css'
createApp(App).use(oyna).mount('#app')
```
Or import only what you use; the rest is left out of your bundle:
```vue
```
The stylesheet is plain CSS. You need neither Tailwind nor UnoCSS.
## Fonts
The look relies on two typefaces: a condensed display face for numbers and headings, and a plain sans for text. The library does not bundle font files, so loading them is one more line. Pick one of three ways.
**From Google Fonts, in one import.** The quickest:
```ts
import 'oyna-ui/style.css'
import 'oyna-ui/fonts.css'
```
The files come from Google's servers. If that is a problem for you — privacy rules, an offline app — use the next way.
**Served by you, with Fontsource.** The fonts become part of your own build:
```bash [npm]
npm install @fontsource/noto-sans @fontsource/barlow-condensed @fontsource/fira-sans-condensed
```
```bash [pnpm]
pnpm add @fontsource/noto-sans @fontsource/barlow-condensed @fontsource/fira-sans-condensed
```
```bash [yarn]
yarn add @fontsource/noto-sans @fontsource/barlow-condensed @fontsource/fira-sans-condensed
```
```bash [bun]
bun add @fontsource/noto-sans @fontsource/barlow-condensed @fontsource/fira-sans-condensed
```
```ts
import '@fontsource/noto-sans/400.css'
import '@fontsource/noto-sans/600.css'
import '@fontsource/noto-sans/700.css'
import '@fontsource/barlow-condensed/700.css'
import '@fontsource/barlow-condensed/800.css'
// only if your interface has Cyrillic text
import '@fontsource/fira-sans-condensed/700.css'
import '@fontsource/fira-sans-condensed/800.css'
```
**Your own typefaces.** Set `--o-font-sans` and `--o-font-display` to whatever you already load; see [Theming](https://oyna-ui.org/guide/theming.md).
| Family | Weights | Used for |
| ------------------- | ------------- | ----------------------------- |
| Noto Sans | 400, 600, 700 | text |
| Barlow Condensed | 700, 800 | numbers and headings |
| Fira Sans Condensed | 700, 800 | Cyrillic numbers and headings |
Load every weight listed: a missing weight is synthesized by the browser and looks lighter.
### Without the fonts
Nothing breaks. Text falls back to the system's own sans, and headings to a condensed face the system has: Avenir Next Condensed on macOS and iOS, Arial Narrow on Windows, Roboto Condensed on Android. It is recognisably the same look, a little less sharp. The same fallbacks show for a moment while the real fonts are on their way.
## Background
The glass needs something behind it. Put [`OBackground`](https://oyna-ui.org/components/background.md) at the top of your app:
```vue
```
## Dependencies
Tabs, Dialog, Select, DropdownMenu, Popover, Tooltip and PinInput are built on [Reka UI](https://reka-ui.com), which handles focus and accessibility. It is installed with the library; you don't set it up.
## What the stylesheet does to the page
Besides the components, `style.css` sets on `body` a zero margin, the text font, white text and the
dark background colour, and makes scrollbars thin and translucent. Nothing else is reset.
## With an AI assistant
An assistant has not seen this library while it was trained, so point it at the docs written for it:
- [`/llms.txt`](https://oyna-ui.org/llms.txt) — what the library is, the rules that are easy to get wrong, and a list of every page;
- [`/llms-full.txt`](https://oyna-ui.org/llms-full.txt) — the whole documentation in one file;
- every page of this site also exists as plain Markdown: add `.md` to its address, as in `/components/button.md`.
Paste the first address into the chat, or add a line to your project's instructions for the assistant: "This project uses Oyna UI; read https://oyna-ui.org/llms.txt before writing interface code."
Source: https://oyna-ui.org/guide/installation
---
# Theming
Every colour, radius and font is a CSS variable. Override them on `:root`, or on any element to restyle one part of the page.
```css
:root {
--o-accent: #4dd2ff;
--o-radius: 10px;
}
```
```vue
PrimarySoftA card with another accent
```
## Tokens
| Variable | Default | Meaning |
| ---------------------------------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------- |
| `--o-accent` | `#a5ff4d` | The one bright colour: the main thing on the screen |
| `--o-danger` | `#ff8a8a` | Something at stake |
| `--o-on-accent` | `#111` | Text on an accent fill |
| `--o-surface` | `rgb(0 0 0 / 0.3)` | Glass |
| `--o-surface-strong` | `rgb(0 0 0 / 0.4)` | Darker glass, for text-heavy content |
| `--o-layer` | `rgb(20 20 26 / 0.8)` | What floats over the page: dialog, popover, menu, tooltip, toast |
| `--o-layer-blur` | `16px` | How much the page is blurred under a layer; `0px` turns it off |
| `--o-fill-1` … `--o-fill-4` | white at 4, 8, 12, 22 % | Stripes, controls, hover, bars |
| `--o-text`, `--o-text-2`, `--o-text-3` | white at 100, 75, 50 % | Text |
| `--o-radius-sm`, `--o-radius`, `--o-radius-lg` | `8px`, `14px`, `24px` | Small controls, cards, dialogs |
| `--o-font-sans` | Noto Sans, then the system sans | Text |
| `--o-font-display` | Barlow Condensed, Fira Sans Condensed, then a condensed system face | Numbers and headings |
| `--o-duration` | `160ms` | Every transition |
| `--o-bg`, `--o-bg-1` … `--o-bg-4` | | [Background](https://oyna-ui.org/components/background.md) |
## With Tailwind or UnoCSS
The library needs neither, but if your project already uses one, the tokens are there as utilities, so the blocks you write yourself match the components. Every name starts with `o-`: nothing of your own theme is replaced. The values are the CSS variables above, so a theme you set with them reaches the utilities without a rebuild.
**Tailwind 4.** One more import after Tailwind's own:
```css
@import 'tailwindcss';
@import 'oyna-ui/tailwind.css';
```
**UnoCSS.** A preset, next to `presetWind3` or `presetWind4`:
```ts
import { defineConfig, presetWind4 } from 'unocss'
import { presetOyna } from 'oyna-ui/unocss'
export default defineConfig({ presets: [presetWind4(), presetOyna()] })
```
Then, in either:
```vue
Deploys
Nothing yet.
```
| Utilities | From |
| ---------------------------------------------------------- | ----------------------------------------------------- |
| `*-o-accent`, `*-o-danger`, `*-o-on-accent` | The accent, the danger colour, text on an accent fill |
| `*-o-surface`, `*-o-surface-strong`, `*-o-layer`, `*-o-bg` | Glass, darker glass, what floats, the page |
| `*-o-fill-1` … `*-o-fill-4` | The white fills |
| `*-o-text`, `*-o-text-2`, `*-o-text-3` | Text at 100, 75 and 50 % |
| `rounded-o-sm`, `rounded-o`, `rounded-o-lg` | The three radii |
| `font-o-sans`, `font-o-display` | The two typefaces |
`*` is any utility that takes a colour: `bg-`, `text-`, `border-`, `ring-`, `fill-`. Tailwind also gets `duration-o` (the library's transition time, zero under reduced motion) and `backdrop-blur-o-layer`.
## Reduced motion
When the user asks for reduced motion, `--o-duration` becomes `0ms` and every transition in the library stops. Use the variable in your own transitions to get the same behaviour.
Source: https://oyna-ui.org/guide/theming
---
# Hotkeys
Hotkeys are read from the physical key (`KeyboardEvent.code`), not from the character it types. `KeyR` is the same key on a QWERTY, an AZERTY and a Russian layout.
A hotkey is ignored:
- while the user types in an `input`, a `textarea`, a `select` or an editable element. A checkbox, a radio and a switch are not typed into, so hotkeys work on them, except Space and the arrow keys, which are their own;
- with Ctrl, Cmd or Alt held, so browser shortcuts keep working;
- on key repeat, when the key is held down;
- for `Enter` and `Space`, when a button or a link has focus: that element handles the key itself.
The numpad Enter counts as `Enter`.
While a [dialog](https://oyna-ui.org/components/dialog.md) or a [popover](https://oyna-ui.org/components/popover.md) is open, only the hotkeys set up inside it work. While a [select](https://oyna-ui.org/components/select.md) or a [dropdown menu](https://oyna-ui.org/components/dropdown-menu.md) is open, or a [key capture](https://oyna-ui.org/components/key-capture.md) waits for a key, none do.
## On a button
The simplest way: [`OButton`](https://oyna-ui.org/components/button.md) and [`OToggle`](https://oyna-ui.org/components/toggle.md) take a `hotkey`, show it and react to it.
```vue
Retry
```
## useHotkey
For anything that is not a button. The listener lives as long as the component.
```vue
```
| Argument | Type | Description |
| ----------------- | --------------------------------------- | -------------------------------------------------------- |
| `code` | `MaybeRefOrGetter` | Physical key, e.g. `KeyR`, `Digit1`, `Enter`, `Escape` |
| `handler` | `(event: KeyboardEvent) => void` | Called on the key press; the default action is prevented |
| `options.enabled` | `MaybeRefOrGetter` | `false` switches the hotkey off |
## hotkeyLabel
`hotkeyLabel(code)` returns what to print for a key: `KeyR` → `R`, `Digit1` → `1`, `Escape` → `Esc`, `ArrowUp` → `↑`, `Slash` → `/`.
The label names the key's position on a QWERTY keyboard. On another layout the same key may carry another letter; pass your own text where that matters.
## On a touch screen
A phone has no keys to show. Where the main pointer is a finger (`pointer: coarse`), Button, Toggle
and the dialog's close button hide their key; the hotkey itself stays registered, so a tablet with a
keyboard attached still reacts to it.
The same media query makes the library easier to hit: small buttons, the crosses of Alert and Tag,
checkboxes, radios and switches get a target of about 44px without changing their shape, segmented
tabs grow taller, and fields use 16px text, so iOS does not zoom the page on focus.
Source: https://oyna-ui.org/guide/hotkeys
---
# Icons
Oyna UI ships no icons. Put any SVG in a component's slot and it takes the size of the text next to it. The examples on this site use [Lucide](https://lucide.dev): restrained line icons that match the look.
```bash [npm]
npm install @lucide/vue
```
```bash [pnpm]
pnpm add @lucide/vue
```
```bash [yarn]
yarn add @lucide/vue
```
```bash [bun]
bun add @lucide/vue
```
```vue
DeployContinue Passed
```
## Where icons are sized for you
| Component | How |
| -------------------------------------------------------------------------------------- | -------------------------------------------------- |
| [Button](https://oyna-ui.org/components/button.md), [Toggle](https://oyna-ui.org/components/toggle.md), [Badge](https://oyna-ui.org/components/badge.md) | An `