CSS Tricks: Creating an Animated Component with CSS + SVG foreignObject
When I received a request to create a text-waving SVG as shown in the picture above, my mind was flooded with terms like svg, path, group, text, and other SVG tags. This was overwhelmingly complicated.
SVG can be quite troublesome. Can’t we just use div+css animations instead? Is there a way to make SVG compatible with common front-end development practices?
Well, it turns out there is. The SVG <foreignObject>
element allows for embedding HTML and CSS within it.
SVG and foreignObject
If you’ve ever looked at SVG code, you might have noticed that every SVG contains an attribute like xmlns="http://www.w3.org/2000/svg
. This is known as a namespace. Without this namespace, the browser interprets the svg tag as text. Don't believe me? Try comparing these two SVGs:
The purpose of the namespace is to inform the browser about the standard it should use for parsing. Without a namespace, the SVG tags are not recognized as belonging to any specific language or format, resulting in them being rendered as ordinary text instead of graphics.
So, what exactly is <foreignObject>
, and how is it used?
<foreignObject>
is an element introduced in the SVG 1.1 specification. It allows embedding elements from other XML namespaces, such as XHTML, within an SVG document. This means developers can now use HTML and CSS inside SVG.
Here’s how it’s used:
<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
<foreignObject x="20" y="20" width="160" height="160">
<div xmlns="http://www.w3.org/1999/xhtml">
<!-- HTML and CSS code -->
</div>
</foreignObject>
</svg>
Specifying the namespace within the <div>
is not mandatory under the HTML5 standard, but it's still recommended for code standardization purposes.
Animation Implementation
With an understanding of <foreignObject>
, we can now use CSS + <foreignObject>
to create the desired animation. This involves three main tasks:
- Dynamic background gradient animation
- Text wobble and fade-in effect
- Configurable content
Dynamic Background Gradient Animation
The dynamic gradient background animation can be achieved using CSS keyframes (@keyframes
)
@keyframes gradientBackground {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
.animate-gradient {
animation: gradientBackground 10s ease infinite;
background: linear-gradient(-45deg, #fc5c7d, #6a82fb, #05dfd7);
background-size: 600% 400%;
width: 100%;
height: 100%;
}
- In
animate-gradient
,animation
specifies the animation style forgradientBackground
.infinite
means it repeats indefinitely. linear-gradient
defines the gradient colors.- Enlarging
background-size
makes fewer colors visible in the area, creating a smoother yet more pronounced visual effect as the animation progresses. - The animation
gradientBackground
is defined with@keyframes
, setting up a cycle of position changes.
The resulting effect is as follows:
Text Wobble and Fade-In
The text wobble effect is also implemented using CSS keyframes (@keyframes
).
@keyframes rotate {
0% { transform: rotate(3deg); }
100% { transform: rotate(-3deg); }
}
.animate-rotate {
animation: rotate ease-in-out 1s infinite alternate;
}
- The wobble angle is defined using
@keyframes
. - The text wobble animation is specified with
animation
, wherealternate
indicates that the animation should alternate directions with each iteration.
The implemented effect is as follows:
Please try to implement the fade-in effect for the small text at the bottom.
Configurable Content
Considering the versatility of this animated card, I decided to make it a configurable component.
Here’s the TypeScript definition:
interface AnimatedSvgComponentProps {
width?: number;
height?: number;
titleSize?: string; // Tailwind CSS text size class
titleText?: string;
paragraphSize?: string;
paragraphText?: string;
paragraphLink?: string; // link attribute
enableAnimation?: boolean;
backgroundColors?: string[];
textColor?: string;
}
Since I’m using Next.js and TailwindCSS in my personal projects, I exported this as a React component. The adjusted component code is as follows:
import React from "react";
const AnimatedSvgComponent: React.FC<AnimatedSvgComponentProps> = ({
width = 800,
height = 400,
titleSize = "text-5xl",
titleText = "Animated SVG<br/>with React & Tailwind",
paragraphSize = "text-xl",
paragraphText = "Click to see the source",
paragraphLink,
enableAnimation = true,
backgroundColors = ["#fc5c7d", "#6a82fb", "#05dfd7"],
textColor = "text-white",
}) => {
const backgroundGradient = `linear-gradient(-45deg, ${backgroundColors.join(
", "
)})`;
const renderedTitle = titleText.split("<br/>").map((line, index) => (
<React.Fragment key={index}>
{line}
<br />
</React.Fragment>
));
const renderedParagraph =
paragraphLink && paragraphText ? (
<a
href={paragraphLink}
target="_blank"
rel="noopener noreferrer"
className={paragraphSize}
>
{paragraphText}
</a>
) : (
<p className={paragraphSize}>{paragraphText}</p>
);
return (
<svg
fill="none"
viewBox={`0 0 ${width} ${height}`}
height={height}
width={width}
xmlns="http://www.w3.org/2000/svg"
>
<foreignObject height={height} width={width}>
<div>
<style>
{`
@keyframes rotate {
0% { transform: rotate(3deg); }
100% { transform: rotate(-3deg); }
}
@keyframes gradientBackground {
0% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
100% { background-position: 0% 50%; }
}
@keyframes fadeIn {
0% { opacity: 0; }
66% { opacity: 0; }
100% { opacity: 1; }
}
.animate-gradient {
animation: gradientBackground 10s ease infinite;
background-size: 600% 400%;
}
.animate-rotate {
animation: rotate ease-in-out 1s infinite alternate;
}
.animate-fadeIn {
animation: fadeIn 3s ease 0s normal forwards;
}
.animated-svg-card {
height: ${height}px;
width: ${width}px;
background: ${backgroundGradient};
background-size: 600% 400%;
animation: gradientBackground 10s ease infinite;
}
`}
</style>
<div
className={`animated-svg-card w-full h-full flex flex-col items-center justify-center m-0 rounded-md ${
enableAnimation ? "animate-gradient" : ""
} ${textColor}`}
>
<h1
className={`${titleSize} uppercase text-shadow ${
enableAnimation ? "animate-rotate" : ""
}`}
>
{renderedTitle}
</h1>
{renderedParagraph}
</div>
</div>
</foreignObject>
</svg>
);
};
export default AnimatedSvgComponent;
There’s room for optimization in the configuration, and you can copy the code and modify it according to your needs.
If you don’t use TailwindCSS, you can get the inline CSS version of the component from my source code repository.
Conclusion
Now that you’ve learned about <foreignObject>
, why not show off your creativity?
To learn more about me and discover additional insights on React, Next.js, and Node, visit https://weijunext.com. You can also explore my Medium articles by visiting https://medium.com/@weijunext for more in-depth content or connect with me on Twitter @ https://twitter.com/weijunext