App Theme Guide
App Theme Guide Use this guide when you build Enfyra App pages, widgets, or extensions and want them to match the admin console in both light and dark mode. The short rule is: choose colors by meaning, not by palette name. Do not pick green , violet , cyan , hex colors, or raw CS
App Theme Guide
Use this guide when you build Enfyra App pages, widgets, or extensions and want them to match the admin console in both light and dark mode.
The short rule is: choose colors by meaning, not by palette name. Do not pick green, violet, cyan, hex colors, or raw CSS variables just because they look close. Use Enfyra classes and Nuxt UI semantic colors so the UI follows the current app theme.
Quick Rules
- Use
color="primary"for the main action or selected identity in the current screen. - Use
color="success",color="warning",color="error", orcolor="info"only when the color communicates status. - Keep large panels neutral. Put status colors on small badges, icons, or short status text inside the panel.
- Use
eapp-surface-*classes for cards, rows, and panels. - Use
eapp-text-*classes for text hierarchy. - Use
eapp-primary-*classes for selected items, active progress, primary icons, and identity accents. - Use
eapp-icon-tilefor square icon tiles instead of making tiles with padding. - Use
CommonModalandCommonDrawerfooter props for standard Cancel, Save, and Delete actions. - Use
UTabsfor tabbed sections instead of building custom tab bars.
Common Choices
| You are building | Use |
|---|---|
| Main save/create/run button | <UButton color="primary" variant="solid" /> |
| Back, refresh, filter, close, or secondary action | <UButton color="neutral" variant="outline" /> or variant="ghost" |
| Destructive action | <UButton color="error" /> |
| Status badge | <UBadge color="success|warning|error|info|neutral" variant="soft" /> |
| Normal card or panel | eapp-surface-card |
| Recessed inner surface or meter track | eapp-surface-muted |
| Clickable row/card hover | eapp-surface-hover |
| Heading or primary value | eapp-text-primary |
| Body copy | eapp-text-secondary |
| Labels, helper text, metadata | eapp-text-tertiary |
| Selected/current entity block | eapp-primary-surface plus normal card chrome such as border and eapp-radius-panel |
| Small selected chip or icon tile | eapp-primary-soft eapp-icon-tile |
| Primary icon or inline primary text | eapp-primary-text |
| Progress fill or active meter | eapp-primary-solid |
| Row dividers | eapp-divide-y |
Modal And Drawer Actions
For a normal modal or drawer footer, pass actions through props. This keeps Cancel, Save, Delete, loading, disabled, and spacing consistent across the app.
<CommonModal
v-model:open="open"
:cancel-action="{ label: 'Cancel', onClick: () => (open = false) }"
:primary-action="{ label: 'Save', loading: saving, disabled: !canSave, onClick: save }"
>
<template #header>
<h3 class="text-lg font-semibold eapp-text-primary">Edit settings</h3>
</template>
<template #body>
<FormEditor v-model="form" table-name="settings" />
</template>
</CommonModal>
Use these action props:
| Prop | Use it for |
|---|---|
cancelAction |
Cancel, discard, or leave-editing actions |
primaryAction |
The main safe action, such as Save, Create, Update, or Run |
dangerAction |
Delete or irreversible destructive action |
leadingActions |
Secondary actions shown on the left side of the footer |
footerHint |
A short helper message beside the footer actions |
Cancel is styled as a neutral outline by default. Use dangerAction for irreversible destructive work such as delete, revoke, or permanently remove.
:cancel-action="{ label: 'Close', tone: 'neutral', onClick: () => (open = false) }"
For discard-confirmation dialogs, the safe action should be primary:
:cancel-action="{ label: 'Keep editing', tone: 'primary', onClick: () => (discardOpen = false) }"
Use a custom #footer only when the footer needs custom layout or non-button content.
For loading placeholders, use USkeleton or shared loading components. The app maps skeleton colors globally so placeholders stay visible in both light and dark mode and follow the current accent without hardcoded palette colors.
Tabs
Use UTabs for page sections, settings panels, and multi-step forms. Enfyra styles active and inactive tab indicators globally, so extension tabs match system pages automatically.
<UTabs
v-model="activeTab"
:items="[
{ label: 'Overview', icon: '', value: 'overview' },
{ label: 'Activity', icon: '', value: 'activity' }
]"
/>
Avoid custom tab bars unless the layout is not really a tabbed interface.
Cards And Lists
Use neutral surfaces for normal content:
<article class="eapp-surface-card p-4">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-sm eapp-text-tertiary">Projects</p>
<p class="mt-2 text-2xl font-semibold eapp-text-primary">{{ total }}</p>
</div>
<span class="eapp-primary-soft eapp-icon-tile">
</span>
</div>
</article>
For rows:
<section class="eapp-surface-card eapp-divide-y">
<button
v-for="item in items"
:key="item.id"
type="button"
class="flex w-full items-center justify-between p-4 text-left eapp-surface-hover"
>
<span class="font-medium eapp-text-primary">{{ item.name }}</span>
<UBadge :color="item.enabled ? 'success' : 'neutral'" variant="soft">
{{ item.enabled ? 'Enabled' : 'Disabled' }}
</UBadge>
</button>
</section>
Selected Or Active Items
Use a primary surface only when the whole block is selected, current, or active. eapp-primary-surface supplies the selected identity tint; keep normal card chrome such as border and eapp-radius-panel on the element too:
<article class="eapp-primary-surface eapp-radius-panel border p-4">
<div class="flex items-center gap-3">
<span class="eapp-primary-soft eapp-icon-tile">
</span>
<div>
<p class="font-semibold eapp-text-primary">{{ name }}</p>
<p class="text-sm eapp-text-tertiary">Currently selected</p>
</div>
</div>
</article>
Do not use eapp-primary-surface for every card in a grid. If everything is highlighted, nothing is highlighted.
Status
Status colors should be small and specific:
<section class="eapp-surface-card p-4">
<div class="flex items-center justify-between gap-3">
<p class="font-semibold eapp-text-primary">Reconciliation</p>
<UBadge color="warning" variant="soft">Needs review</UBadge>
</div>
</section>
Avoid making the whole panel green, yellow, or red. Use a neutral panel with a status badge inside it.
Progress
Use a neutral track and primary fill:
<div class="h-1.5 overflow-hidden eapp-radius-pill eapp-surface-muted">
<div class="h-full eapp-primary-solid" :style="{ width: progressWidth }"></div>
</div>
What To Avoid
Do not write:
<UButton color="violet">Save</UButton>
<UBadge color="green">Active</UBadge>
<div class="bg-slate-950 text-gray-200 border-gray-700"></div>
<div class="text-[var(--some-color)] bg-[var(--some-bg)]"></div>
<div style="color: #14b8a6"></div>
Write:
<UButton color="primary">Save</UButton>
<UBadge color="success">Active</UBadge>
<div class="eapp-surface-card eapp-text-primary"></div>
This lets the same extension look correct when the operator changes the app accent color or switches between light and dark mode.