Skip to content
UniAria
Color scheme
Contrast
Color scheme
Contrast

Getting started

Install

Pick the adapter for your stack. Each brings @uniaria/core, where the behavior definitions live.

React

pnpm add @uniaria/core @uniaria/react

React 18 or 19.

Vue

pnpm add @uniaria/core @uniaria/vue

Vue 3.5 or later.

Custom elements

pnpm add @uniaria/core @uniaria/elements

For plain HTML, server templates, and any framework that renders HTML.

The packages are ES modules only and target ES2022. They support the browsers in Baseline widely available, and every entry imports safely on a server.

React

Import a component’s definition from its subpath of @uniaria/core, pass it to useBehavior with props, and spread each part’s props onto your element:

import { button } from '@uniaria/core/button';
import { useBehavior } from '@uniaria/react';

export function SaveButton({ onSave }: { onSave(): void }) {
  const save = useBehavior(button, { onPress: onSave });
  return <button {...save.parts.root}>Save</button>;
}

save.parts.root carries the attributes, event handlers, a ref and the marker data-uni="button", which your CSS can select. Add your own attributes next to it, className included: class is all yours. Leave ARIA state and the data-* attributes to UniAria: it writes them itself, and warns in development when you pass one.

A component with several parts gives you one set of props per part:

import { toggleSwitch } from '@uniaria/core/switch';
import { useBehavior } from '@uniaria/react';

export function WifiSwitch() {
  const wifi = useBehavior(toggleSwitch, {
    defaultSelected: true,
    labelElementType: 'span',
    onSelectedChange: (isSelected) => console.log(isSelected),
  });
  return (
    <label {...wifi.parts.root}>
      <input {...wifi.parts.input} name="wifi" />
      <span {...wifi.parts.indicator} />
      <span {...wifi.parts.label}>Wi-Fi</span>
    </label>
  );
}

Hovering, pressing or toggling the switch doesn’t render WifiSwitch again: UniAria writes the new state straight to the elements. When your render needs a value, select it with useBehaviorState. See State.

Vue

The same definitions work in Vue. Pass one to useBehavior in <script setup>, with props, and bind each part’s props onto your element with v-bind:

<script setup lang="ts">
import { button } from '@uniaria/core/button';
import { useBehavior } from '@uniaria/vue';

const emit = defineEmits<{ save: [] }>();
const save = useBehavior(button, { onPress: () => emit('save') });
</script>

<template>
  <button v-bind="save.parts.root">Save</button>
</template>

As in React, save.parts.root carries the attributes, event handlers, a ref and the data-uni="button" marker. Write your own attributes after v-bind, class included, and leave ARIA state and the data-* attributes to UniAria. Props that change come as a getter, which reads your props and refs like computed() does:

<script setup lang="ts">
import { toggleSwitch } from '@uniaria/core/switch';
import { useBehavior } from '@uniaria/vue';

const props = defineProps<{ isOffline: boolean }>();

const wifi = useBehavior(toggleSwitch, () => ({
  defaultSelected: true,
  isDisabled: props.isOffline,
  labelElementType: 'span',
}));
</script>

<template>
  <label v-bind="wifi.parts.root">
    <input v-bind="wifi.parts.input" name="wifi" />
    <span v-bind="wifi.parts.indicator" />
    <span v-bind="wifi.parts.label">Wi-Fi</span>
  </label>
</template>

Toggling the switch doesn’t render this component again either. When your template needs a value, read it with useBehaviorState. See Vue.

Custom elements

Import a component’s registering entry once, anywhere on the page. It defines the element, <uni- plus the component name, and wraps your markup. Mark the root part with data-uni and the component name, and every other part with data-element, its name capitalized (Input, Indicator):

<uni-button id="save">
  <button data-uni="button">Save</button>
</uni-button>

<script type="module">
  import '@uniaria/elements/button';

  document.querySelector('#save').addEventListener('press', () => save());
</script>

The element has no shadow root and renders nothing: your <button> stays the button, styled by your CSS. Props become attributes in kebab case, without an is or should prefix (isDisabled is disabled), and properties with the prop’s own name (element.isDisabled). Events are lowercase DOM events with the value in detail (pressstart, selectedchange).

The element reads its starting state from your markup where HTML has it, such as a checkbox’s checked or a link’s href, so server-rendered HTML works before JavaScript runs.

Another prefix

To register a component under your own name, call defineElement with its definition instead of importing the registering entry:

import { button } from '@uniaria/core/button';
import { defineElement } from '@uniaria/elements';

defineElement(button, { prefix: 'acme' }); // <acme-button>

The parts keep their markers: the root is still data-uni="button".

In other frameworks

The elements work wherever HTML does. Tell your framework that uni-* tags are custom elements if it asks (Vue’s compilerOptions.isCustomElement, for example, though Vue has its own adapter too), listen for the lowercase events, and set controlled values as properties:

<!-- Vue -->
<uni-switch :isSelected="wifi" @selectedchange="wifi = $event.detail">
  <label data-uni="switch">
    <input data-element="Input" type="checkbox" />
    <span data-element="Indicator"></span>
    <span data-element="Label">Wi-Fi</span>
  </label>
</uni-switch>

Next steps

  • Styling: the attributes and variables your CSS can target.
  • State: controlled values, events and reading state.
  • Components: every component with a live demo.