oriUI

Join

A thin wrapper that collapses the shared radii and borders of adjacent controls so they read as one segmented unit — button groups, input+button combos, segmented toolbars. No visual styling of its own: all colour, size, and variant tokens live on the children.

The examples are organised by layer: the class reference is the standalone @oriui/css layer, and the Framework API is the @oriui/vue component. Every example is live — flip its code between HTML (the standalone classes, also your htmx / Astro / Svelte / plain-HTML usage), Vue; HTML is the default.

Classes

ClassTypeDescription
ori-joinBlockRequired base class. inline-flex container; collapses shared radii and borders between adjacent children.
ori-join_verticalModifierStack children vertically instead of horizontally. Collapses block edges instead of inline edges.
role=groupSemanticsAlways present on the root element. Wrap logically related controls; supply aria-label or aria-labelledby so screen readers announce the group purpose.

Horizontal button group

Three outline buttons joined into one segmented control. The shared interior borders collapse to a single 1 px line; the outer corners keep their radius.

html
<div class="ori-join" role="group" aria-label="Text alignment">
    <button class="ori-button ori-variant_outline ori-color_primary">Left</button>
    <button class="ori-button ori-variant_outline ori-color_primary">Center</button>
    <button class="ori-button ori-variant_outline ori-color_primary">Right</button>
</div>
vue
<OriJoin aria-label="Text alignment">
    <OriButton variant="outline" text="Left" />
    <OriButton variant="outline" text="Center" />
    <OriButton variant="outline" text="Right" />
</OriJoin>

An input field joined with a submit button — the canonical search-box pattern. The shared border between the field and the button merges into one line.

html
<div class="ori-join" role="group" aria-label="Search">
    <input class="ori-input" type="search" placeholder="Search…" aria-label="Search query" />
    <button class="ori-button ori-variant_fill ori-color_primary">Go</button>
</div>
vue
<OriJoin aria-label="Search">
    <OriInput placeholder="Search…" aria-label="Search query" />
    <OriButton variant="fill" text="Go" />
</OriJoin>

Vertical

vertical stacks children in a column and collapses the block edges instead of the inline edges.

html
<div class="ori-join ori-join_vertical" role="group" aria-label="View mode">
    <button class="ori-button ori-variant_outline ori-color_primary">List</button>
    <button class="ori-button ori-variant_outline ori-color_primary">Grid</button>
    <button class="ori-button ori-variant_outline ori-color_primary">Table</button>
</div>
vue
<OriJoin :vertical="true" aria-label="View mode">
    <OriButton variant="outline" text="List" />
    <OriButton variant="outline" text="Grid" />
    <OriButton variant="outline" text="Table" />
</OriJoin>

Common patterns

A tonal colour swatch group and a quantity stepper — two everyday compositions showing how variant and color on the children drive all the visual weight.

Colour swatch group

html
<div class="ori-join" role="group" aria-label="Highlight colour">
    <button class="ori-button ori-variant_tonal ori-color_danger">Red</button>
    <button class="ori-button ori-variant_tonal ori-color_warn">Amber</button>
    <button class="ori-button ori-variant_tonal ori-color_success">Green</button>
    <button class="ori-button ori-variant_tonal ori-color_info">Blue</button>
</div>
vue
<!-- Colour swatch toolbar — tonal buttons, each its own semantic colour -->
<OriJoin aria-label="Highlight colour">
    <OriButton variant="tonal" color="danger" text="Red" />
    <OriButton variant="tonal" color="warn" text="Amber" />
    <OriButton variant="tonal" color="success" text="Green" />
    <OriButton variant="tonal" color="info" text="Blue" />
</OriJoin>

Quantity stepper

html
<div class="ori-join" role="group" aria-label="Quantity">
    <button class="ori-button ori-variant_outline ori-color_primary" aria-label="Decrease quantity">−</button>
    <input
        class="ori-input"
        type="number"
        value="1"
        aria-label="Current quantity"
        style="width: 4rem; text-align: center"
    />
    <button class="ori-button ori-variant_outline ori-color_primary" aria-label="Increase quantity">+</button>
</div>
vue
<!-- Quantity stepper: decrement · value display · increment -->
<OriJoin aria-label="Quantity">
    <OriButton variant="outline" text="−" aria-label="Decrease quantity" />
    <OriInput :value="1" aria-label="Current quantity" style="width: 4rem; text-align: center" />
    <OriButton variant="outline" text="+" aria-label="Increase quantity" />
</OriJoin>

Accessibility

The accessibility contract holds across every layer — the standalone classes and the Vue component render the same markup.

  • The root element always carries role="group". A group that contains multiple controls should have an accessible name so screen readers announce its purpose. Pass aria-label (or aria-labelledby referencing a visible heading) directly on <OriJoin> — it falls through to the root element unchanged.
  • Children keep their own roles, labels, and keyboard behaviour. The join wrapper adds no tab stop and intercepts no keys.
  • When a child is focused or hovered its z-index is raised to 1 so its full :focus-visible ring is not clipped by the adjacent collapsed border.
  • Icon-only children still need their own aria-label.
KeyAction
TabMoves focus between child controls.
EnterActivates the focused child control.
SpaceActivates a focused button child.
Arrow keysWithin a radiogroup child, moves between options.

Framework API

The props, events, and slots of the Vue component. The standalone CSS layer has no component API — its surface is the classes above.

Props

PropTypeDefaultDescription
asstring | object'div'Element or component to render. Accepts any HTML tag name or a Component reference.
verticalbooleanfalseStack children in a column and collapse block edges instead of inline edges.

Events & attributes

OriJoin declares no custom events and does not set inheritAttrs: false, so native attributes (aria-label, aria-labelledby, id, class, style, data-*) fall through to the root element. Pass aria-label here to name the group.

Slots

SlotDescription
defaultThe controls to group — buttons, inputs, or any element that carries ori-button / ori-input classes.

Polymorphic (as)

Render any tag or component. Useful when the join is semantically a toolbar (role="toolbar" via aria-label on an <OriJoin as="div">) or when a router wrapper is needed.

vue
<!-- Use a semantic toolbar role for a group of action buttons -->
<OriJoin as="div" role="toolbar" aria-label="Formatting options">
    <OriButton variant="outline" text="Bold" aria-pressed="false" />
    <OriButton variant="outline" text="Italic" aria-pressed="false" />
    <OriButton variant="outline" text="Underline" aria-pressed="false" />
</OriJoin>