Skip to content

Video Text

Play a video inside the shapes of your text.

OCEAN
View source on GitHub

Installation

Copy the component below into src/components/spark-ui/video-text/video-text.vue.

vue
<script setup lang="ts">
import { Comment, isVNode, useSlots } from "vue";

interface VideoTextProps {
  src: string;
  as?: string;
  className?: string;
  autoPlay?: boolean;
  muted?: boolean;
  loop?: boolean;
  preload?: "auto" | "metadata" | "none";
  fontSize?: string | number;
  fontWeight?: string | number;
  textAnchor?: string;
  dominantBaseline?: string;
  fontFamily?: string;
}
const props = withDefaults(defineProps<VideoTextProps>(), {
  as: "div",
  autoPlay: true,
  muted: true,
  loop: true,
  preload: "auto",
  fontSize: 20,
  fontWeight: "bold",
  textAnchor: "middle",
  dominantBaseline: "middle",
  fontFamily: "sans-serif",
});
const slots = useSlots();
function textContent(nodes: unknown[]): string {
  return nodes
    .map((node): string => {
      if (typeof node === "string" || typeof node === "number") return String(node);
      if (Array.isArray(node)) return textContent(node);
      if (isVNode(node) && node.type !== Comment) {
        return Array.isArray(node.children)
          ? textContent(node.children)
          : typeof node.children === "string"
            ? node.children
            : "";
      }
      return "";
    })
    .join("");
}
function escapeXml(value: string | number): string {
  return String(value).replace(
    /[&<>"']/g,
    (character) =>
      ({
        "&": "&amp;",
        "<": "&lt;",
        ">": "&gt;",
        '"': "&quot;",
        "'": "&apos;",
      })[character]!,
  );
}
function maskStyle() {
  const size = typeof props.fontSize === "number" ? `${props.fontSize}vw` : props.fontSize;
  const content = textContent(slots.default?.() ?? []);
  const mask = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"><text x="50%" y="50%" font-size="${escapeXml(size)}" font-weight="${escapeXml(props.fontWeight)}" text-anchor="${escapeXml(props.textAnchor)}" dominant-baseline="${escapeXml(props.dominantBaseline)}" font-family="${escapeXml(props.fontFamily)}">${escapeXml(content)}</text></svg>`;
  const image = `url("data:image/svg+xml,${encodeURIComponent(mask)}")`;
  return { maskImage: image, WebkitMaskImage: image };
}
</script>

<template>
  <component :is="as" class="relative size-full" :class="className">
    <div
      class="video-text-mask absolute inset-0 flex items-center justify-center"
      :style="maskStyle()"
      aria-hidden="true"
    >
      <video
        :key="src"
        :src="src"
        class="h-full w-full object-cover"
        :autoplay="autoPlay"
        :muted="muted"
        :loop="loop"
        :preload="preload"
        playsinline
      />
    </div>
    <span class="sr-only"><slot /></span>
  </component>
</template>

<style scoped>
.video-text-mask {
  mask-size: contain;
  -webkit-mask-size: contain;
  mask-repeat: no-repeat;
  -webkit-mask-repeat: no-repeat;
  mask-position: center;
  -webkit-mask-position: center;
}
</style>

Usage

vue
<script setup lang="ts">
import VideoText from "@/components/spark-ui/video-text/video-text.vue";
</script>

<template>
  <div class="relative h-56 w-full">
    <VideoText src="https://cdn.magicui.design/ocean-small.webm">OCEAN</VideoText>
  </div>
</template>

Props

PropTypeDefaultDescription
srcstringRequiredVideo URL.
asstring"div"Wrapper HTML tag.
classNamestringUnsetExtra wrapper classes.
autoPlaybooleantrueRequests automatic playback.
mutedbooleantrueMutes the video.
loopbooleantrueRepeats the video.
preload"auto" | "metadata" | "none""auto"Browser loading hint.
fontSizestring | number20Number in SVG viewport width units, or an SVG font-size string.
fontWeightstring | number"bold"Weight of the text.
textAnchorstring"middle"Horizontal text alignment.
dominantBaselinestring"middle"Vertical text alignment.
fontFamilystring"sans-serif"Font for the text.

Behavior

The default slot supplies the text. Use plain text or text inside HTML elements. The mask uses the text without its HTML formatting.

An SVG mask hides the video outside the letters. The video remains an HTML video element with inline playback. A hidden copy of the text supports screen readers.

Give the parent an explicit height and width. The mask scales with its container and updates when the text or font props change. Special characters stay literal text.

The demo loads https://cdn.magicui.design/ocean-small.webm. Your browser must support WebM and allow automatic playback. If your site limits media or image sources, allow the video host and SVG data: images. No extra package is required.

Source

Ported from Magic UI Video Text. The upstream source uses React.

Released under the MIT License.