I was experimenting with gradient borders and stumbled across an interesting technique โ cards that adapt to the content inside them. You can see a demo of the effect in action here.
Here's what we'll be building today:
This card will magically adapt it's colours to the emoji passed to it, but a similar approach could be used for any kind of content.
Let's run through the steps from scratch and break it down!
Initialise the Project
We'll be building from the ground up without abstracting anything. The tech stack we'll be using is:
- Vite with React and TypeScript: To quickly set up our project environment.
- Chakra UI: It's not necessary for the effect, but Chakra's style props make styling easier.
- Framer Motion: This is a peer dependency of Chakra UI and will come in handy if we decide to animate the card.
Set up the project with Vite: Start by running the following:
npm create vite@latest
Choose React with TypeScript when prompted.
Install Chakra UI and Framer Motion:
npm i @chakra-ui/react @emotion/react @emotion/styled framer-motion
Create a Chakra theme: Save the following in theme.ts
:
import { ThemeConfig, extendTheme } from '@chakra-ui/react';
const config: ThemeConfig = {
initialColorMode: 'dark',
useSystemColorMode: false,
};
const theme = extendTheme({
config,
styles: {
global: () => ({
body: {
bg: '#121212',
},
}),
},
});
export default theme;
This sets the Chakra colour scheme to dark mode and applies a dark grey background.
Integrate the Chakra Provider: Modify src/main.tsx
to include the Chakra provider:
import { ChakraProvider } from '@chakra-ui/react';
import theme from './theme';
...
<React.StrictMode>
<ChakraProvider theme={theme}>
<App />
</ChakraProvider>
</React.StrictMode>
Styling Adjustments: Ensure your page takes up the entire screen by setting min-height: 100vh;
on the #root
element within App.css
.
Build the Basic Card
Let's lay the foundation by first creating a basic card component.
Create the Component: Create a src/components/EmojiCard.tsx
component:
import { Box, BoxProps } from '@chakra-ui/react';
interface Props {
emoji: string;
}
export default function EmojiCard({ emoji, children }: Props) {
return (
/* main container */
<Box position="relative" maxW={700} borderRadius={8} bg="#181818">
/* content wrapper */
<Box
px={8}
py={4}
gap={{ base: 0, sm: 8 }}
display="flex"
alignItems="center"
flexDirection={{ base: 'column', sm: 'row' }}
borderRadius={6}
>
/* emoji container */
<Box
display="flex"
alignItems="center"
justifyContent="center"
fontSize="80px"
width="200px"
>
{emoji}
</Box>
/* text content container */
<Box height="100%" textAlign={{ base: 'center', sm: 'left' }}>
{children}
</Box>
</Box>
</Box>
);
}
Box
is a component from Chakra UI that allows us to pass in style props directly.
In this basic setup we have a surrounding main container with a constrained width and a background. We also have a content wrapper on the inside with a place for our emoji and for the text content.
This code serves as a base structure for our card.
Render the Component: Update App.tsx
to render our new component:
import EmojiCard from "./components/EmojiCard";
...
<EmojiCard emoji="๐ฌ">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Commodi explicabo doloremque, accusantium repellat dolorem natus soluta quos!
</EmojiCard>
Now we have something to work with!
The Effect
With the initial setup done, let's get into the fun part โ creating the effect!
Background Emoji: Add the emoji as a background within the main container but before the content wrapper:
<Box
position="absolute"
left={0}
top={0}
width="100%"
height="100%"
justifyContent={'center'}
alignItems={'center'}
display="flex"
_before={{
content: `"${emoji}"`,
fontSize: 80,
}}
/>;
This places a duplicate emoji in the middle of the card. Notice how it goes over the content even though the background component is before our content in the code.
Because the background element has an absolute
position, and our content has a default static
position, CSS's stacking context puts the background over the content. To fix this, set the content wrapper box to position="relative"
.
Styling Tweaks: Increase the emoji size by adjusting the font size up to 1100, and add a filter to the emoji background element: filter={"blur(80px)"}
This is an interesting gradient effect, but we want to confine it to our card. Add an overflow="hidden"
to the main container.
Now let's add the following to the content wrapper to cut out a space for content in the middle, and get a subtle border with these gradient colours:
backgroundColor="#151515"
border="2px solid transparent"
backgroundClip="padding-box"
The interesting part here is the backgroundClip
property. It determines how far the background extends within an element.
padding-box
means the backgroundColor
we set will extend to the outer edge of the padding, but won't go under the border.
This card should now respond to any emoji you pass into it and use it to fill the background gradient:
Add Polish
Let's enhance this effect further!
Gradient Background: Replace the backgroundColor
property we've set with the following:
backgroundImage="linear-gradient(rgb(20 20 20 / 0.8), rgb(20 20 20))"
This will replace the solid background with a light gradient, slightly transparent at the top, so that some of the colour from the background can shine through:
Animate the Background: We can make the card a little more dynamic and eye-catching by animating the emoji background using Framer Motion:
import { motion, useTime, useTransform } from 'framer-motion';
const AnimatedBox = motion(Box);
...
// inside our component
const time = useTime();
const rotate = useTransform(
time,
[0, 16000], // every 16 seconds...
[0, 360], // ...rotate 360deg
{ clamp: false }, // repeat the animation
);
...
// convert our emoji background to a framer motion component
<AnimatedBox
// pass in our special framer motion value
style={{ rotate }}
...
We convert the Box
component for the emoji background into an AnimatedBox
using Framer Motion's motion
utility. This allows the component to accept special values that Framer Motion animates for us outside of React's standard rendering cycle.
We use the useTime
and useTransform
hooks from Framer Motion to calculate how much the emoji should rotate. Then we pass the rotate
value as a style prop to our Framer Motion component to handle the animation.
This is the final result in action.
Well done for making it this far! You can check out the final code for this on the CodeSandbox here, and the original experimentation demo shows it in action with some different emojis.
It's exciting to stumble across techniques like this. We've just scratched the surface, and there are lots of different directions you could take.
Imagine this effect with different light modes, or as an icon button that self-styles based on the provided icon.
You're not restricted to emojis; throw in any image, pattern, or coloured text. Experiment with different borders. Play around with the filter blur โ remove it, or use another.
Let us know in the comments how you went!