Avatar

An image element with a fallback for representing a user.
CU
Installation
Expand

npx @left4code/cosmic-ui-cli@latest add avatar
 
Expand

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:

pnpm
npm
yarn
bun
yarn add @zag-js/avatar @zag-js/vue

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.

components/ui/avatar/context.ts
Expand

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 };
 
Expand
components/ui/avatar/AvatarRoot.vue
Expand

<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>
 
Expand
components/ui/avatar/AvatarImage.vue
Expand

<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>
 
Expand
components/ui/avatar/AvatarFallback.vue
Expand

<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>
 
Expand
components/ui/avatar/index.ts
Expand

export { default as AvatarRoot } from "./AvatarRoot.vue";
export { default as AvatarImage } from "./AvatarImage.vue";
export { default as AvatarFallback } from "./AvatarFallback.vue";
 
Expand

Update the import paths to match your project setup.

Usage
Expand

import { AvatarRoot, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
 
Expand
Expand

<AvatarRoot>
  <AvatarImage src="https://i.pravatar.cc/150?u=cosmic-ui" alt="User avatar" />
  <AvatarFallback>CU</AvatarFallback>
</AvatarRoot>
 
Expand
Powered by synthetic caffeine · Deployed by Left4code · Signal traceable on GitHub.