Skip to content
Orbix Vue · Tailwind CSS v1.0.3

Using components

Orbix components are plain template markup with fd-* classes (BEM naming) and, for interactive ones, a data-fd-* attribute that the bundled behaviors pick up automatically when the page is mounted (usePageScript()): there is nothing else to initialize. The UI, Forms and Tables pages of the template show every component; open the page component under src/pages/ to copy its markup. Reusable blocks are typed single-file components in src/components/ (<ProductCard>, <MailRow>, <Password>, <Otp>, <Example>...): their parameters are props, and blocks that take markup (actions, badges) use slots.

The design system also ships Vue components for its main contracts (@foundry/ui/vue): FdIcon, FdButton, FdInput, FdTextarea, FdSelect, FdCheckbox, FdSwitch, FdField, FdCard, FdBadge, FdAlert, FdAvatar, FdTable, FdPagination, FdTabs, FdDropdown, FdModal, FdToast, FdToaster, FdTooltip, FdSkeleton, FdEmptyState and FdStatCard. They render the same fd-* markup, take every text from props or slots, and are a quick way to build your own pages:

<script setup lang="ts">
import { ref } from 'vue';
import { FdButton, FdField, FdInput } from '@foundry/ui/vue';
import { useI18n } from '@/composables/useI18n';

const { t } = useI18n();
const email = ref('');
</script>

<template>
  <FdField id="email" :label="t('common.fields.email')" v-slot="{ id }">
    <FdInput :id="id" v-model="email" type="email" autocomplete="email" />
  </FdField>
  <FdButton variant="primary" icon="check">{{ t('common.actions.save') }}</FdButton>
</template>

The template pages use FdIcon for the design system icons; the snippets below show the plain markup, which is what the pages contain. Replace the translation keys with your own (see Languages); t and fmt come from useI18n().

Buttons

<button type="button" class="fd-btn fd-btn--primary"><span class="fd-btn__label">{{ t('common.actions.save') }}</span></button>
<button type="button" class="fd-btn fd-btn--secondary"><span class="fd-btn__label">{{ t('common.actions.cancel') }}</span></button>
<button type="button" class="fd-btn fd-btn--ghost fd-btn--sm"><span class="fd-btn__label">{{ t('common.actions.preview') }}</span></button>
<button type="button" class="fd-btn fd-btn--danger"><span class="fd-btn__label">{{ t('common.actions.delete') }}</span></button>
<button type="button" class="fd-btn fd-btn--ghost fd-btn--icon" :aria-label="t('shell.settings')">
  <svg class="fd-icon" aria-hidden="true"><use href="#lc-settings" /></svg>
</button>

Cards

<article class="fd-card" aria-labelledby="card-title">
  <header class="fd-card__header">
    <div class="fd-card__heading">
      <h2 class="fd-card__title" id="card-title">{{ t('reports.revenue.title') }}</h2>
      <p class="fd-card__subtitle">{{ t('reports.revenue.period') }}</p>
    </div>
  </header>
  <div class="fd-card__body"><p class="fd-text-muted">{{ t('reports.revenue.text') }}</p></div>
  <footer class="fd-card__footer">
    <button type="button" class="fd-btn fd-btn--primary fd-btn--sm"><span class="fd-btn__label">{{ t('reports.revenue.open') }}</span></button>
  </footer>
</article>

Stat (KPI) tiles

fmt() formats the value for the active language (number, currency, percent...):

<div class="fd-kpis">
  <section class="fd-stat" aria-labelledby="kpi-orders">
    <div class="fd-stat__header">
      <h2 class="fd-stat__label" id="kpi-orders">{{ t('nav.orders') }}</h2>
      <span class="fd-stat__icon" aria-hidden="true"><svg class="fd-icon"><use href="#lc-shopping-bag" /></svg></span>
    </div>
    <p class="fd-stat__value">{{ fmt('currency', 48250, { digits: 0 }) }}</p>
  </section>
</div>

Alerts and badges

<div class="fd-alert fd-alert--success" role="status">
  <FdIcon name="success" class="fd-alert__icon" />
  <div class="fd-alert__content">
    <p class="fd-alert__title">{{ t('billing.paid_title') }}</p>
    <p>{{ t('billing.paid_text') }}</p>
  </div>
</div>

<span class="fd-badge fd-badge--success">{{ t('common.status.paid') }}</span>
<span class="fd-badge fd-badge--warning">{{ t('common.status.pending') }}</span>
<span class="fd-badge fd-badge--danger">{{ t('common.status.overdue') }}</span>

Form fields

<div class="fd-field">
  <label class="fd-field__label" for="email">{{ t('common.fields.email') }}</label>
  <input class="fd-input" id="email" type="email" autocomplete="email" v-model="email" />
</div>
<div class="fd-field">
  <label class="fd-field__label" for="website">{{ t('common.fields.website') }}</label>
  <div class="fd-input-wrap">
    <svg class="fd-icon fd-input-wrap__icon" aria-hidden="true"><use href="#lc-globe" /></svg>
    <input class="fd-input" id="website" type="url" v-model="website" />
  </div>
</div>

The demo forms are plain HTML forms with initial values; in your application bind the fields with v-model (as above) or a form library as you prefer – the markup and classes stay the same. The password field with a visibility toggle and the one-time code input are components: <Password id="new-password" meter="true" /> and <Otp /> (src/components/forms/).

Tables

<div class="fd-table-wrap" tabindex="0" role="region" :aria-label="t('nav.invoices')">
  <table class="fd-table">
    <thead>
      <tr><th scope="col">{{ t('common.fields.invoice') }}</th><th scope="col">{{ t('common.fields.customer') }}</th><th scope="col">{{ t('common.fields.status') }}</th></tr>
    </thead>
    <tbody>
      <tr v-for="invoice in invoices" :key="invoice.id">
        <td>{{ invoice.number }}</td>
        <td>{{ invoice.customer }}</td>
        <td><span class="fd-badge fd-badge--success">{{ t('common.status.paid') }}</span></td>
      </tr>
    </tbody>
  </table>
</div>

The Tables → Data table page shows the data grid with sorting, search, column filters, selection, export and pagination (src/scripts/tables.ts). In your own pages use the useDataGrid composable and bind the returned template ref:

<script setup lang="ts">
import { useDataGrid } from '@/composables/useDataGrid';

const gridEl = useDataGrid<Invoice>({ rows: invoices, rowId: (r) => r.id, columns: [/* key, label, sortable, render */] });
</script>

<template>
  <div ref="gridEl" class="fd-datatable"><!-- toolbar, table and footer markup as on the Data table page --></div>
</template>

Modals

<button type="button" class="fd-btn fd-btn--primary" data-fd-modal-open="modal-invite" aria-haspopup="dialog">
  <span class="fd-btn__label">{{ t('pages.team.invite') }}</span>
</button>

<dialog class="fd-modal fd-modal--sm" id="modal-invite" aria-labelledby="modal-invite-title" data-fd-modal>
  <div class="fd-modal__header">
    <div class="fd-modal__heading"><h2 class="fd-modal__title" id="modal-invite-title">{{ t('pages.team.invite') }}</h2></div>
  </div>
  <div class="fd-modal__body"><p>{{ t('pages.team.invite_text') }}</p></div>
  <div class="fd-modal__footer">
    <button type="button" class="fd-btn fd-btn--secondary" data-fd-modal-close><span class="fd-btn__label">{{ t('common.close') }}</span></button>
  </div>
</dialog>

From code: openModal(dialog) and closeModal(dialog) from @foundry/ui; confirmations with await confirmDialog({ title, body, confirmLabel, cancelLabel, tone: 'danger' }). The FdModal component of @foundry/ui/vue is the same dialog, opened and closed with v-model (a boolean).

Tabs

<div class="fd-tabs" data-fd-tabs>
  <div class="fd-tabs__list" role="tablist" :aria-label="t('nav.settings')">
    <button type="button" class="fd-tabs__tab" role="tab" id="tab-general" aria-controls="panel-general" aria-selected="true">{{ t('settings.general') }}</button>
    <button type="button" class="fd-tabs__tab" role="tab" id="tab-billing" aria-controls="panel-billing" aria-selected="false" tabindex="-1">{{ t('nav.billing') }}</button>
  </div>
  <div class="fd-tabs__panel" role="tabpanel" id="panel-general" aria-labelledby="tab-general" tabindex="0">…</div>
  <div class="fd-tabs__panel" role="tabpanel" id="panel-billing" aria-labelledby="tab-billing" tabindex="0" hidden>…</div>
</div>
<div class="fd-dropdown" data-fd-dropdown>
  <button type="button" class="fd-btn fd-btn--secondary" aria-haspopup="menu" aria-expanded="false" aria-controls="menu-actions">
    <span class="fd-btn__label">{{ t('common.actions.actions') }}</span>
  </button>
  <div class="fd-menu" role="menu" id="menu-actions" hidden>
    <button type="button" class="fd-menu__item" role="menuitem" tabindex="-1" @click="edit"><span class="fd-menu__label-text">{{ t('common.actions.edit') }}</span></button>
    <hr class="fd-menu__separator" />
    <button type="button" class="fd-menu__item fd-menu__item--danger" role="menuitem" tabindex="-1" @click="remove"><span class="fd-menu__label-text">{{ t('common.actions.delete') }}</span></button>
  </div>
</div>

The menu also dispatches an fd:select event with the chosen item; regular @click handlers on the items are the simplest option in Vue.

Toasts

<script setup lang="ts">
import { toast } from '@foundry/ui';
import { useI18n } from '@/composables/useI18n';

const { t } = useI18n();
const save = () => toast({ title: t('account.saved'), variant: 'success', closeLabel: t('common.dismiss') });
</script>

<template>
  <button type="button" class="fd-btn fd-btn--secondary" @click="save">
    <span class="fd-btn__label">{{ t('common.actions.save') }}</span>
  </button>
</template>

Grid

Responsive 12-column grid with logical gaps:

<div class="fd-row">
  <div class="fd-col-12 fd-col-md-6 fd-col-xl-4">…</div>
  <div class="fd-col-12 fd-col-md-6 fd-col-xl-8">…</div>
</div>

Accessibility

Components ship with the right roles and keyboard support (menus, tabs, modals, comboboxes, date pickers). Keep the aria-* attributes from the examples, give every icon-only button an aria-label, and use aria-hidden="true" on decorative icons (FdIcon adds it for you).