Alternative UI Variants
The same component data can render in multiple visual ways. A NavigationLink might be a plain text link, a filled button, or a YouTube icon — all backed by the same label and url fields. An admin picks the variant from the manager panel.
There are two approaches. Use style classes for variations that are purely CSS differences. Use UI templates for variations that need fundamentally different markup.
Approach 1: Style Classes
Define named class sets in useCwaComponent. An admin can select one of these styles from the manager panel.
<script setup lang="ts">
import { computed } from 'vue'
import type { IriProp } from '#cwa/composables/cwa-resource'
import { useCwaComponent } from '#imports'
const props = defineProps<IriProp>()
const { resource, exposeMeta, getCurrentStyleName } = useCwaComponent(props, [], {
styles: {
multiple: false,
classes: {
'Default': '',
'Filled Button': 'bg-blue-600 text-white px-6 py-2 rounded-md',
'Outlined Button': 'border border-blue-600 text-blue-600 px-6 py-2 rounded-md',
}
}
})
const currentStyleName = computed(() => {
if (!resource.value?.data) return 'Default'
return getCurrentStyleName(resource.value.data)
})
defineExpose(exposeMeta)
</script>
<template>
<a :href="resource?.data?.url || '#'">
{{ resource?.data?.label }}
</a>
</template>
How it works
styles.classes — a map of style names to a class string (e.g. 'bg-blue-600 text-white'). A string[] is also accepted and joined, but the string form is canonical. The name (e.g. 'Filled Button') is what appears in the manager panel as an option; the classes it maps to are written to the component's uiClassNames, one entry per selected style.
styles.multiple — false means the admin picks one style. true allows multiple styles to be combined.
getCurrentStyleName(resource.value.data) — returns the currently applied style name (e.g. 'Filled Button'). Use this to drive conditional logic — different icons, different element types, different aria attributes.
Style classes are applied automatically to the component's root element — no :class binding needed. To apply them to an inner element instead, pass { autoClass: false } and bind the returned uiClassNames ref manually.
Conditional rendering based on style
For small variations — toggling an icon, changing an aria attribute, adjusting a minor class difference — using currentStyleName is fine:
<template>
<!-- Show an arrow icon only on filled buttons -->
<a :href="resource?.data?.url">
{{ resource?.data?.label }}
<ArrowIcon v-if="currentStyleName === 'Filled Button'" />
</a>
</template>
currentStyleName to conditionally render fundamentally different HTML structure. If you find yourself showing completely different elements, different component hierarchies, or different semantic markup based on the style name, create a ui/ template instead. Style names can be renamed by the developer; ui/ filenames are stable identifiers. Conditional markup based on a name that could change is a maintenance hazard.Approach 2: UI Templates (ui/ directory)
When a variant needs completely different HTML structure — not just different classes — create an alternative Vue file in a ui/ subdirectory.
The ui/ file
<!-- app/cwa/components/NavigationLink/ui/YouTube.vue -->
<template>
<a :href="resource?.data?.url">
<YouTubeIcon class="w-15 h-10" />
</a>
</template>
<script setup lang="ts">
import type { IriProp } from '#cwa/composables/cwa-resource'
import { useCwaComponent } from '#imports'
const props = defineProps<IriProp>()
const { resource, exposeMeta } = useCwaComponent(props, [], {
name: 'YouTube' // optional — sets the label in the admin UI selector
})
defineExpose(exposeMeta)
</script>
No extra registration is needed. The module discovers ui/YouTube.vue from its file location and registers it as the YouTube variant automatically. When an admin selects YouTube in the manager panel, this file is rendered instead of NavigationLink.vue.
The name option is purely cosmetic — it sets the human-readable label shown in the admin "UI" selector. Without it, the selector falls back to the raw component name (CwaComponentNavigationLinkUiYouTube). It does not need to match the filename.
When to use ui/ vs styles.classes
Use a ui/ template when the variant needs different HTML structure — different elements, different child components, fundamentally different markup. The YouTube icon variant above is the classic case: it's not "NavigationLink but with different classes," it's a completely different thing that happens to share the same data.
Use styles.classes when it's the same HTML with different Tailwind classes, or minor conditional logic (toggling an icon, changing an aria label, a small class tweak).
Real Example: HtmlContent in the Template App
The NavigationLink code above is illustrative. For working examples of both approaches, look at the HtmlContent component in the template app.
app/cwa/components/HtmlContent/HtmlContent.vue declares one style, with multiple: true so it can be combined with any others you add:
const { resource, exposeMeta, $cwa } = useCwaComponent(props, undefined, {
styles: {
multiple: true,
classes: {
'Black Background': 'bg-black border border-white p-2',
},
},
})
app/cwa/components/HtmlContent/ui/AltHtmlContent.vue is a genuinely different template — a bordered <article> with a logo image above the content — registered with { name: 'Alternative' }, which is the label the admin's UI selector shows.
So an admin editing an HTML content block picks the default template or Alternative, and on the default can toggle the Black Background style. The style options come from whichever template is selected, so a ui/ variant that declares no styles shows no style options.