Shimmer
Animated sweep over streaming or loading text; used for status lines inside a ghost bubble.
Thinking...
Generating your plan
Calling get_prices
Thinking...
Generating your plan
Calling get_prices
Thinking...
Generating your plan
Calling get_prices
import { Shimmer } from "@/components/ai/shimmer";
export default function Demo() { return ( <div className="flex flex-wrap items-center gap-10"> <Shimmer>Thinking...</Shimmer> <Shimmer as="h2" className="text-2xl font-medium"> Generating your plan </Shimmer> <Shimmer duration={1} spread={4}> Calling get_prices </Shimmer> </div> );}<script setup lang="ts">import { Shimmer } from "@/components/ai/shimmer";</script>
<template> <div class="flex flex-wrap items-center gap-10"> <Shimmer>Thinking...</Shimmer> <Shimmer as="h2" class="text-2xl font-medium">Generating your plan</Shimmer> <Shimmer :duration="1" :spread="4">Calling get_prices</Shimmer> </div></template><script lang="ts"> import { Shimmer } from "$lib/components/ai/shimmer";</script>
<div class="flex flex-wrap items-center gap-10"> <Shimmer>Thinking...</Shimmer> <Shimmer as="h2" class="text-2xl font-medium">Generating your plan</Shimmer> <Shimmer duration={1} spread={4}>Calling get_prices</Shimmer></div>Polymorphic (as), uses motion.
Installation
Section titled “Installation”npx shadcn@latest add @edmi-ui/ai-shimmerpnpm dlx shadcn@latest add @edmi-ui/ai-shimmeryarn dlx shadcn@latest add @edmi-ui/ai-shimmerbunx shadcn@latest add @edmi-ui/ai-shimmerPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install motionTerminal window pnpm add motionTerminal window yarn add motionTerminal window bun add motionCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-vue@latest add @edmi-ui/ai-shimmerpnpm dlx shadcn-vue@latest add @edmi-ui/ai-shimmeryarn dlx shadcn-vue@latest add @edmi-ui/ai-shimmerbunx shadcn-vue@latest add @edmi-ui/ai-shimmerPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Install the dependencies:
Terminal window npm install motion-vTerminal window pnpm add motion-vTerminal window yarn add motion-vTerminal window bun add motion-vCopy the code from Component source into your project and update the import paths to match your aliases.
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-shimmer.jsonpnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-shimmer.jsonyarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-shimmer.jsonbunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/ai-shimmer.jsonPick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.
Copy the code from Component source into your project and update the import paths to match your aliases.
import { Shimmer } from "@/components/ai/shimmer";
<div className="flex flex-wrap items-center gap-10"> <Shimmer>Thinking...</Shimmer> <Shimmer as="h2" className="text-2xl font-medium"> Generating your plan </Shimmer> <Shimmer duration={1} spread={4}> Calling get_prices </Shimmer></div><script setup lang="ts">import { Shimmer } from "@/components/ai/shimmer";</script>
<template> <div class="flex flex-wrap items-center gap-10"> <Shimmer>Thinking...</Shimmer> <Shimmer as="h2" class="text-2xl font-medium">Generating your plan</Shimmer> <Shimmer :duration="1" :spread="4">Calling get_prices</Shimmer> </div></template><script lang="ts"> import { Shimmer } from "$lib/components/ai/shimmer";</script>
<div class="flex flex-wrap items-center gap-10"> <Shimmer>Thinking...</Shimmer> <Shimmer as="h2" class="text-2xl font-medium">Generating your plan</Shimmer> <Shimmer duration={1} spread={4}>Calling get_prices</Shimmer></div>Component source
Section titled “Component source”"use client";
import { cn } from "cn";import type { MotionProps } from "motion/react";import { motion } from "motion/react";import type { CSSProperties, ElementType, JSX } from "react";import { memo, useMemo } from "react";
type MotionHTMLProps = MotionProps & Record<string, unknown>;
// Cache motion components at module level to avoid creating them during render.const motionComponentCache = new Map< keyof JSX.IntrinsicElements, React.ComponentType<MotionHTMLProps>>();
const getMotionComponent = (element: keyof JSX.IntrinsicElements) => { let component = motionComponentCache.get(element); if (!component) { component = motion.create(element); motionComponentCache.set(element, component); } return component;};
export interface TextShimmerProps { children: string; as?: ElementType; className?: string; /** Seconds per sweep. */ duration?: number; /** Highlight width per character, in px. */ spread?: number;}
// The sweep is a --foreground highlight over --muted-foreground text; the 0-alpha stops only shape the// gradient inside the text clip (no surface is transparent, DESIGN §4.16).const ShimmerComponent = ({ children, as: Component = "p", className, duration = 2, spread = 2,}: TextShimmerProps) => { const MotionComponent = getMotionComponent( Component as keyof JSX.IntrinsicElements, );
const dynamicSpread = useMemo( () => (children?.length ?? 0) * spread, [children, spread], );
return ( <MotionComponent animate={{ backgroundPosition: "0% center" }} className={cn( "relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent", "[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--foreground),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]", className, )} data-slot="ai-shimmer" initial={{ backgroundPosition: "100% center" }} style={ { "--spread": `${dynamicSpread}px`, backgroundImage: "var(--bg), linear-gradient(var(--muted-foreground), var(--muted-foreground))", } as CSSProperties } transition={{ duration, ease: "linear", repeat: Number.POSITIVE_INFINITY, }} > {children} </MotionComponent> );};
export const Shimmer = memo(ShimmerComponent);<script setup lang="ts">import type { Component, CSSProperties, HTMLAttributes } from "vue"import { motion } from "motion-v"import { computed, useSlots } from "vue"import { cn } from "@/registry/edmi/lib/utils"
export interface TextShimmerProps { as?: keyof HTMLElementTagNameMap class?: HTMLAttributes["class"] /** Seconds per sweep. */ duration?: number /** Highlight width per character, in px. */ spread?: number}
const props = withDefaults(defineProps<TextShimmerProps>(), { as: "p", duration: 2, spread: 2,})
const slots = useSlots()
const textContent = computed(() => { const defaultSlot = slots.default?.() if (!defaultSlot || defaultSlot.length === 0) return "" return defaultSlot .map(vnode => (typeof vnode.children === "string" ? vnode.children : "")) .join("")})
const dynamicSpread = computed(() => (textContent.value?.length ?? 0) * props.spread)
// The sweep is a --foreground highlight over --muted-foreground text; the 0-alpha stops only shape the// gradient inside the text clip (no surface is transparent, DESIGN §4.16).const componentClasses = computed(() => cn( "relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent", "[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--foreground),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]", props.class,))
const componentStyle = computed((): CSSProperties => ({ "--spread": `${dynamicSpread.value}px`, "backgroundImage": "var(--bg), linear-gradient(var(--muted-foreground), var(--muted-foreground))",}))
const MotionComponent = computed(() => (motion[props.as as keyof typeof motion] || motion.p) as Component)</script>
<template> <component :is="MotionComponent" data-slot="ai-shimmer" :class="componentClasses" :style="componentStyle" :initial="{ backgroundPosition: '100% center' }" :animate="{ backgroundPosition: '0% center' }" :transition="{ repeat: Number.POSITIVE_INFINITY, duration, ease: 'linear', }" > <slot /> </component></template>export { default as Shimmer } from "./Shimmer.vue";import Shimmer from "./shimmer.svelte";
export { Shimmer };<script lang="ts"> import { cn } from "$lib/utils.js"; import type { Snippet } from "svelte"; import type { HTMLAttributes } from "svelte/elements";
let { children, as = "p", class: className, duration = 2, spread = 2, ...restProps }: HTMLAttributes<HTMLElement> & { children: Snippet; as?: keyof HTMLElementTagNameMap; /** Seconds per sweep. */ duration?: number; /** Highlight width per character, in px. */ spread?: number; } = $props();
// A snippet cannot be measured, so the highlight width follows the rendered text length. let el = $state<HTMLElement | null>(null); let length = $state(0); $effect(() => { if (!el) return; const sync = () => (length = el?.textContent?.length ?? 0); sync(); const observer = new MutationObserver(sync); observer.observe(el, { characterData: true, childList: true, subtree: true }); return () => observer.disconnect(); });</script>
<!-- The sweep is a --foreground highlight over --muted-foreground text; the 0-alpha stops only shape the gradient inside the text clip (no surface is transparent, DESIGN §4.16). --><svelte:element this={as} bind:this={el} data-slot="ai-shimmer" class={cn( "ai-shimmer relative inline-block bg-[length:250%_100%,auto] bg-clip-text text-transparent", "[--bg:linear-gradient(90deg,#0000_calc(50%-var(--spread)),var(--foreground),#0000_calc(50%+var(--spread)))] [background-repeat:no-repeat,padding-box]", className )} style="--spread: {length * spread}px; --shimmer-duration: {duration}s; background-image: var(--bg), linear-gradient(var(--muted-foreground), var(--muted-foreground)); background-position: 100% center;" {...restProps}> {@render children()}</svelte:element>
<style> @keyframes ai-shimmer-sweep { from { background-position: 100% center; } to { background-position: 0% center; } }
.ai-shimmer { animation: ai-shimmer-sweep var(--shimmer-duration, 2s) linear infinite; }</style>| Prop | Type | Default |
|---|---|---|
children |
string |
|
as |
ElementType |
p |
duration |
number (seconds per sweep) |
2 |
spread |
number (px per character) |
2 |
✦ Edmi additions
Section titled “✦ Edmi additions”- The sweep is a
--foregroundhighlight over--muted-foreground, so it reads in light and dark.