Tui components contain HTML that is structured semantically, using the HTML5 standard and provides containing wrappers enough to allow for a wide range of theming needs. Much of our HTML now, or in the future, needs to serve more than one purpose on a given web page once it is rendered. Because of this, we manipulate HTML by embedding logic within the markup, to control application of CSS selectors, ARIA attributes, conditional component trees and more.


We follow the principle of "material honesty": we use most appropriate element for the job, and only use a div or span if no other element fits. If it's for navigation (it goes somewhere), we use a link. If it's for an action (it does something), we use a button. We use radios and checkboxes for multiple choice options. We use HTML5 input types (email, number, password, search, tel, url). Grids use divs by default (since they are mostly used as containers for presentation), but can be set to use other more meaningful tags (such as aside or ul and li) if appropriate.

We add ARIA roles where necessary: this is mostly just for custom components that don't have a native HTML element.


Here's a link being used as navigation.

<a href="/server/totara/competency/rate_competencies.php?user_id=1">Rate competencies</a>

Here's a button being used for action, with type="button" so that it doesn't submit the form it's inside of.

<button type="button">Show filters</button>

For form controls, our framework renders a label with a for attribute, programmatically linking it to the relevant input. For required fields, we show an asterisk, but hide it for screen reader users, replacing it with meaningful text (classes and other attributes omitted for clarity).

<label for="uid-1">Title
	<span title="Required"><span aria-hidden="true">*</span> <span class="sr-only">Required</span></span>

<input id="uid-1" name="title" type="text">

For groups of controls, we use role="radiogroup" to group the controls and aria-labelledby to programmatically link the group to its visible name appearing earlier in the DOM.

<label id="uid-1-label">Favourite colour</label>
<div role="radiogroup" aria-labelledby="uid-1-label">
	<input id="uid-1" type="radio" name="favouritecolor" value="red">
	<label for="uid-1">Red</label>

	<input id="uid-1" type="radio" name="favouritecolor" value="green">
	<label for="uid-1">Green</label>

	<input id="uid-1" type="radio" name="favouritecolor" value="blue">
	<label for="uid-1">Blue</label>

Tips and known limitations

  • One limitation of Vue 2 is that components must have a single root element in their template. This element can change (e.g. with a v-if) but there must only be one at once - this is enforced both by the linter and runtime warnings.
  • If you need to have more than one root element, a workaround is sometimes possible - if your component doesn't have any state, you can define it as a functional component, which can have multiple root elements.
  • Avoid writing invalid HTML as this will cause issues with server-side rendering - for example, if you put a <div> inside of a <p>, the <div> will be moved outside the <p> by the browser, meaning it will no longer match and cause will cause hydration errors on page load.

Recommended reading