<AvatarRoot>
<AvatarImage src="https://i.pravatar.cc/150?u=cosmic-ui" alt="User avatar" />
<AvatarFallback>CU</AvatarFallback>
</AvatarRoot>
npx @left4code/cosmic-ui-cli@latest add avatar
This writes the component and everything it imports, then installs the packages it needs. Run npx @left4code/cosmic-ui-cli@latest initfirst if you haven't already (it detects Vue automatically).
Install the following dependencies:
Built directly on the Zag.js state machine. The fallback and image both render unconditionally - the machine toggles a hidden attribute on whichever one shouldn't show, based on whether the image loaded. The square border shown here is a placeholder; swap it for the Frame primitive once its avatar-specific paths exist. Vue SFCs can't export multiple components from one file, so Avatar is a folder instead of a single file — copy all of it into your project.
import type { InjectionKey, ComputedRef } from "vue";
import type * as avatarMachine from "@zag-js/avatar";
type AvatarApi = ComputedRef<ReturnType<typeof avatarMachine.connect>>;
const AvatarApiKey: InjectionKey<AvatarApi> = Symbol("avatar-api");
export { AvatarApiKey };
export type { AvatarApi };
<script setup lang="ts">
import { computed, provide, useAttrs, useId } from "vue";
import { useMachine, normalizeProps } from "@zag-js/vue";
import * as avatarMachine from "@zag-js/avatar";
import { twMerge } from "tailwind-merge";
import { AvatarApiKey } from "./context";
defineOptions({ inheritAttrs: false });
const rawAttrs = useAttrs() as Partial<avatarMachine.Props> & { class?: string };
const id = useId();
const machineProps = computed(() => {
const { class: _class, ...rest } = rawAttrs;
return { id, ...rest };
});
const service = useMachine(avatarMachine.machine, machineProps);
const api = computed(() => avatarMachine.connect(service, normalizeProps));
provide(AvatarApiKey, api);
</script>
<template>
<span
v-bind="api.getRootProps()"
:class="
twMerge([
'relative inline-flex size-10 items-center justify-center overflow-hidden border border-primary/30 bg-primary/10',
rawAttrs.class,
])
"
>
<slot />
</span>
</template>
<script setup lang="ts">
import { inject } from "vue";
import { twMerge } from "tailwind-merge";
import { AvatarApiKey } from "./context";
const { src, alt, class: className } = defineProps<{
src: string;
alt: string;
class?: string;
}>();
const injectedApi = inject(AvatarApiKey);
if (!injectedApi) throw new Error("Avatar parts must be used within <AvatarRoot>");
const api = injectedApi;
</script>
<template>
<img
v-bind="api.getImageProps()"
:src="src"
:alt="alt"
:class="twMerge(['size-full object-cover', className])"
/>
</template>
<script setup lang="ts">
import { inject } from "vue";
import { twMerge } from "tailwind-merge";
import { AvatarApiKey } from "./context";
defineProps<{ class?: string }>();
const injectedApi = inject(AvatarApiKey);
if (!injectedApi) throw new Error("Avatar parts must be used within <AvatarRoot>");
const api = injectedApi;
</script>
<template>
<span v-bind="api.getFallbackProps()" :class="twMerge(['font-medium', $props.class])">
<slot />
</span>
</template>
export { default as AvatarRoot } from "./AvatarRoot.vue";
export { default as AvatarImage } from "./AvatarImage.vue";
export { default as AvatarFallback } from "./AvatarFallback.vue";
Update the import paths to match your project setup.
import { AvatarRoot, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
<AvatarRoot>
<AvatarImage src="https://i.pravatar.cc/150?u=cosmic-ui" alt="User avatar" />
<AvatarFallback>CU</AvatarFallback>
</AvatarRoot>