Getting started
Pick the adapter for your stack. Each brings @uniaria/core, where the
behavior definitions live.
React
pnpm add @uniaria/core @uniaria/reactReact 18 or 19.
Vue
pnpm add @uniaria/core @uniaria/vueVue 3.5 or later.
Custom elements
pnpm add @uniaria/core @uniaria/elementsFor 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.
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.
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.
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.
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".
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>- Styling: the attributes and variables your CSS can target.
- State: controlled values, events and reading state.
- Components: every component with a live demo.