{"id":7957,"date":"2025-12-04T14:30:21","date_gmt":"2025-12-04T19:30:21","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=7957"},"modified":"2025-12-04T14:30:22","modified_gmt":"2025-12-04T19:30:22","slug":"the-deep-card-conundrum","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/the-deep-card-conundrum\/","title":{"rendered":"The Deep Card Conundrum"},"content":{"rendered":"\n<p>In the world of web design, we often talk about &#8220;cards&#8221;. Those neat little rectangles that group information together are the bread and butter of modern UI. But usually, these cards are as flat as the screens they live on. Maybe they have a subtle drop shadow to hint at elevation, but that\u2019s where the illusion ends.<\/p>\n\n\n\n<p>But what if a card wasn&#8217;t just a surface? What if it was a <em>window<\/em>?<\/p>\n\n\n\n<p>Enter the <strong>Deep Card<\/strong>.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_ZYWrKoP\/ebbe7336c4eac3acf7e9fde40387c8bc\" src=\"\/\/codepen.io\/anon\/embed\/ZYWrKoP\/ebbe7336c4eac3acf7e9fde40387c8bc?height=450&amp;theme-id=1&amp;slug-hash=ZYWrKoP\/ebbe7336c4eac3acf7e9fde40387c8bc&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed ZYWrKoP\/ebbe7336c4eac3acf7e9fde40387c8bc\" title=\"CodePen Embed ZYWrKoP\/ebbe7336c4eac3acf7e9fde40387c8bc\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Imagine a card that isn&#8217;t just a 2D plane, but a container with actual volume. A card that holds a miniature 3D world inside it. When you rotate this card, you don&#8217;t just see it skew, you see the elements inside it shift in perspective, revealing their depth. It\u2019s like holding a glass box filled with floating objects.<\/p>\n\n\n\n<p>The effect is mesmerizing. It transforms a static element into something tactile and alive. It invites interaction. Whether it&#8217;s for a digital trading card game, a premium product showcase, or just a portfolio piece that screams &#8220;look at me,&#8221; the Deep Card adds a layer of polish and &#8220;wow&#8221; factor that flat design simply can&#8217;t match.<\/p>\n\n\n\n<p>But as I quickly discovered, building this illusion, especially one that feels right and performs smoothly, is a bit more of a puzzle than it first appears.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-css-trap\">The CSS Trap<\/h2>\n\n\n\n<p>There are plenty of JavaScript libraries out there that can handle this, but I\u2019m a bit of a CSS purist (read: stubborn). I\u2019ve spent years pushing stylesheets to their absolute limits, and I was convinced that a clean, performant, pure CSS solution was hiding in plain sight.<\/p>\n\n\n\n<p>On paper, the logic seems flawless. If you\u2019ve dabbled in 3D CSS before, you know the drill:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Set the Stage<\/strong>: Take a container element and give it some&nbsp;<code>perspective<\/code>.<\/li>\n\n\n\n<li><strong>Build the World<\/strong>: Position the child elements in 3D space (<code>translateZ<\/code>,&nbsp;<code>rotateX<\/code>, etc.).<\/li>\n\n\n\n<li><strong>Preserve the Illusion<\/strong>: Apply&nbsp;<code>transform-style: preserve-3d<\/code>&nbsp;so all those children share the same 3D space.<\/li>\n<\/ol>\n\n\n\n<p>Simple, right?<\/p>\n\n\n\n<p>But here\u2019s the catch. For a true &#8220;card&#8221; effect, you need the content to stay <em>inside<\/em> the card boundaries. If a 3D star floats &#8220;up&#8221; towards the viewer, you don&#8217;t want it to break the frame, you want it to be clipped by the card&#8217;s edges, reinforcing the idea that it&#8217;s inside a container.<\/p>\n\n\n\n<p>So, naturally, you add <code>overflow: clip<\/code> (or <code>hidden<\/code>) to the card. And that is the exact moment everything falls apart.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"567\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/12\/t0LtCaNQ.png?resize=1024%2C567&#038;ssl=1\" alt=\"Comparison of overflow properties in CSS: left shows 'overflow: visible;' with layered rectangles, right shows 'overflow: clip;' with clipped edges.\" class=\"wp-image-7960\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/12\/t0LtCaNQ.png?resize=1024%2C567&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/12\/t0LtCaNQ.png?resize=300%2C166&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/12\/t0LtCaNQ.png?resize=768%2C426&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/12\/t0LtCaNQ.png?w=1200&amp;ssl=1 1200w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_RNaQZew\/af221d197a7af667776c8a1936e186e6\" src=\"\/\/codepen.io\/anon\/embed\/RNaQZew\/af221d197a7af667776c8a1936e186e6?height=450&amp;theme-id=1&amp;slug-hash=RNaQZew\/af221d197a7af667776c8a1936e186e6&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed RNaQZew\/af221d197a7af667776c8a1936e186e6\" title=\"CodePen Embed RNaQZew\/af221d197a7af667776c8a1936e186e6\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-spec-says-no\">The Spec Says No<\/h2>\n\n\n\n<p>Suddenly, your beautiful 3D scene flattens out. The depth vanishes. The magic is gone.<\/p>\n\n\n\n<p>Why? Because according to the&nbsp;<a href=\"https:\/\/www.w3.org\/TR\/css-transforms-2\/#grouping-property-values\">CSS Transforms Module Level 2 specification<\/a>, applying any &#8220;grouping property&#8221; like&nbsp;<code>overflow<\/code>&nbsp;(with any value other than&nbsp;<code>visible<\/code>),&nbsp;<code>opacity<\/code>&nbsp;less than 1, or&nbsp;<code>filter<\/code>, forces the element to flatten.<\/p>\n\n\n\n<p class=\"learn-more\"><strong>The sad reality:<\/strong> A value of <code>preserve-3d<\/code> for <code>transform-style<\/code> is ignored if the element has any grouping property values.<\/p>\n\n\n\n<p>In other words: you can have a 3D container, or you can clip its content. <strong>You cannot do both on the same element.<\/strong><\/p>\n\n\n\n<p>For a long time, this felt like a dead end. How do you keep the 3D depth while keeping the elements contained?!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"faking-it\"><strong>Faking It<\/strong><\/h2>\n\n\n\n<p>If the spec says we can&#8217;t have both perspective and clipping, maybe we can cheat. If we can&#8217;t use real 3D depth, perhaps we can fake it.<\/p>\n\n\n\n<p>Faking perspective is a time-honored tradition in 2D graphics. You can simulate depth by manipulating the size and position of elements based on their &#8220;distance&#8221; from the viewer. In CSS terms, this means using <code>scale()<\/code> to make things smaller as they get &#8220;further away&#8221; and <code>translate()<\/code> to move them relative to the card&#8217;s angle.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.card<\/span> {\n  <span class=\"hljs-comment\">\/* --mouse-x and --mouse-y values ranage from -1 to 1 *\/<\/span>\n  <span class=\"hljs-attribute\">--tilt-x<\/span>: <span class=\"hljs-built_in\">calc<\/span>(var(--mouse-y, <span class=\"hljs-number\">0.1<\/span>) * -<span class=\"hljs-number\">120deg<\/span>); \n  <span class=\"hljs-attribute\">--tilt-y<\/span>: <span class=\"hljs-built_in\">calc<\/span>(var(--mouse-x, <span class=\"hljs-number\">0.1<\/span>) * <span class=\"hljs-number\">120deg<\/span>); \n  <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">rotateX<\/span>(var(--tilt-x)) <span class=\"hljs-built_in\">rotateY<\/span>(var(--tilt-y));\n}\n\n<span class=\"hljs-selector-class\">.card-layer<\/span> {\n  <span class=\"hljs-comment\">\/* Fake perspective with scale and translate *\/<\/span>\n  <span class=\"hljs-attribute\">scale<\/span>: <span class=\"hljs-built_in\">calc<\/span>(<span class=\"hljs-number\">1<\/span> - var(--i) * <span class=\"hljs-number\">0.02<\/span>);\n  <span class=\"hljs-attribute\">translate<\/span>:\n    <span class=\"hljs-built_in\">calc<\/span>(var(--mouse-x) * (<span class=\"hljs-built_in\">var<\/span>(--i)) * -<span class=\"hljs-number\">20%<\/span>)\n    <span class=\"hljs-built_in\">calc<\/span>(var(--mouse-y) * (<span class=\"hljs-built_in\">var<\/span>(--i)) * -<span class=\"hljs-number\">20%<\/span>);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-1\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This technique can work wonders. There are some brilliant examples out there, like <a href=\"https:\/\/x.com\/jh3yy\/status\/1987670585133187417\">this one by Jhey<\/a>, that pull off the effect beautifully without using a single line of <code>perspective<\/code> or <code>preserve-3d<\/code>.<\/p>\n\n\n\n<p>It\u2019s a solid approach. It\u2019s performant, it works across browsers, and for subtle effects, it\u2019s often indistinguishable from the real thing.<\/p>\n\n\n\n<p><strong>But it has a ceiling.<\/strong><\/p>\n\n\n\n<p>The illusion holds up well within a limited range of motion. But the moment you push it too far, say, by rotating the card to a sharp angle or trying to flip it 180 degrees, the math starts to show its cracks. The perspective flattens out, and the movement stops feeling natural.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_raeJPLq\/35bcca4b4b9aa9f3e8c8a2d52b989181\" src=\"\/\/codepen.io\/anon\/embed\/raeJPLq\/35bcca4b4b9aa9f3e8c8a2d52b989181?height=450&amp;theme-id=1&amp;slug-hash=raeJPLq\/35bcca4b4b9aa9f3e8c8a2d52b989181&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed raeJPLq\/35bcca4b4b9aa9f3e8c8a2d52b989181\" title=\"CodePen Embed raeJPLq\/35bcca4b4b9aa9f3e8c8a2d52b989181\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>As you can see, when the card turns, the inner elements lose their spatial relationship. The magic evaporates. So while this is a great tool for the toolbox, it wasn&#8217;t the complete solution I was looking for. I wanted the real deal. Full 3D, full rotation, full clipping.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"road-to-a-nowhere\"><strong>Road to a Nowhere<\/strong><\/h2>\n\n\n\n<p>I spent years (on and off, I\u2019m not <em>that<\/em> obsessed) trying to crack this. I was convinced there had to be a way to have my cake and eat it too.<\/p>\n\n\n\n<p>Theoretically, there <em>is<\/em> a way. If you can&#8217;t clip the container, you have to clip the children.<\/p>\n\n\n\n<p>Imagine using <code>clip-path<\/code> on every single layer inside the card. You would need to calculate, in real-time, exactly where the edges of the card are relative to the viewer, and then apply a dynamic clipping mask to each child element so that it cuts off exactly at those boundaries.<\/p>\n\n\n\n<p>This involves a lot of math, even for me. We\u2019re talking about projecting 3D coordinates onto a 2D plane, calculating intersections, and handling the trigonometry of the user\u2019s perspective.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"512\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/12\/Rw8ilzFF.png?resize=1024%2C512&#038;ssl=1\" alt=\"A textured blackboard covered with various mathematical equations, diagrams, and symbols, creating a complex academic backdrop.\" class=\"wp-image-7961\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/12\/Rw8ilzFF.png?w=1024&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/12\/Rw8ilzFF.png?resize=300%2C150&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/12\/Rw8ilzFF.png?resize=768%2C384&amp;ssl=1 768w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>I was almost ready to give up and accept that maybe, just maybe, this was one of those things CSS just wasn&#8217;t meant to do. And then, I got a message from <a href=\"https:\/\/x.com\/CubiqNation\">Cubiq<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-breakthrough\"><strong>The Breakthrough<\/strong><\/h2>\n\n\n\n<p>This wasn&#8217;t the first time someone had asked me about this topic. As someone who\u2019s known for pushing CSS 3D to its limits, I get this question a lot. People assume I have the answer. But, well&#8230; I didn&#8217;t.<\/p>\n\n\n\n<p>So when Cubiq messaged me, showing me a GIF of a fully rotating card with deep 3D elements and asking how to achieve it, I went into auto-pilot. I gave him the standard explanation on why the spec forbids it, why <code>overflow<\/code> flattens the context, and how he could try to fake it with <code>scale<\/code> and <code>translate<\/code>.<\/p>\n\n\n\n<p>I thought that was the end of it, but then, he surprised me.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"602\" height=\"230\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/12\/image.png?resize=602%2C230&#038;ssl=1\" alt=\"A screenshot of a messaging app conversation featuring a user's message expressing excitement about a discovery.\" class=\"wp-image-7969\" style=\"width:585px;height:auto\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/12\/image.png?w=602&amp;ssl=1 602w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/12\/image.png?resize=300%2C115&amp;ssl=1 300w\" sizes=\"auto, (max-width: 602px) 100vw, 602px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"my-personal-blind-spot\"><strong>My Personal Blind Spot<\/strong><\/h2>\n\n\n\n<p>I\u2019ve tried many tricks over the years, but one property I religiously avoided was <code>perspective-origin<\/code>.<\/p>\n\n\n\n<p>If you really dig into how CSS calculates perspective, you realize that <code>perspective-origin<\/code> doesn&#8217;t just shift your point of view. It fundamentally skews the entire viewport. It creates this weird, unnatural distortion that usually looks terrible.<\/p>\n\n\n\n<p class=\"learn-more\">I cover this at length in my talk <a href=\"https:\/\/www.youtube.com\/watch?v=LzDf8BizhmQ\">3D in CSS, and the True Meaning of Perspective<\/a>, if you\u2019re into that sort of thing.<\/p>\n\n\n\n<p>Cubiq, however, didn&#8217;t have my baggage. He looked at the problem with fresh eyes and realized something brilliant: just as <code>perspective-origin<\/code> can be used to <em>create<\/em> distortion, it can also be used to <em>correct<\/em> it.<\/p>\n\n\n\n<p class=\"learn-more\">Alternate blog post title idea: <strong>Finally, we found one good use for <code>perspective-origin<\/code>!<\/strong> \ud83e\udd23<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-solution\"><strong>The Solution<\/strong><\/h2>\n\n\n\n<p>Here is the magic formula that Cubiq came up with:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-2\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.card-container<\/span> {\n  <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">rotateX<\/span>(var(--tilt-x)) <span class=\"hljs-built_in\">rotateY<\/span>(var(--tilt-y));\n}\n\n<span class=\"hljs-selector-class\">.card<\/span> {\n  <span class=\"hljs-attribute\">perspective<\/span>: <span class=\"hljs-built_in\">calc<\/span>(\n    cos(var(--tilt-x)) * <span class=\"hljs-built_in\">cos<\/span>(var(--tilt-y)) * <span class=\"hljs-built_in\">var<\/span>(--perspective)\n  );\n  <span class=\"hljs-attribute\">perspective-origin<\/span>: \n    <span class=\"hljs-built_in\">calc<\/span>(cos(var(--tilt-x)) * <span class=\"hljs-built_in\">sin<\/span>(var(--tilt-y)) * <span class=\"hljs-built_in\">var<\/span>(--perspective) * -<span class=\"hljs-number\">1<\/span> + <span class=\"hljs-number\">50%<\/span>)\n    <span class=\"hljs-built_in\">calc<\/span>(sin(var(--tilt-x)) * <span class=\"hljs-built_in\">var<\/span>(--perspective) + <span class=\"hljs-number\">50%<\/span>);\n  <span class=\"hljs-attribute\">overflow<\/span>: clip;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-2\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>It looks a bit scary at first glance, but the logic is actually quite elegant.<\/p>\n\n\n\n<p>Since we are using <code>overflow: clip<\/code>, the 3D context is flattened. This means the browser treats the card as a flat surface and renders its children onto that surface. Normally, this flattening would kill the 3D effect of the children. They would look like a flat painting on a rotating wall.<\/p>\n\n\n\n<p>But here is the trick: <strong>We use<\/strong> <code>perspective<\/code> <strong>and<\/strong> <code>perspective-origin<\/code> <strong>to counter-act the rotation.<\/strong><\/p>\n\n\n\n<p>By dynamically calculating the <code>perspective-origin<\/code> based on the card&#8217;s tilt, we are essentially telling the browser: &#8220;Hey, I know you flattened this element, but I want you to render the perspective of its children <em>as if<\/em> the viewer is looking at them from this specific angle.&#8221;<\/p>\n\n\n\n<p>We are effectively projection-mapping the 3D scene onto the 2D surface of the card. The math ensures that the projection aligns perfectly with the card&#8217;s physical rotation, creating the illusion of a deep, 3D space inside a container that the browser considers &#8220;flat.&#8221;<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_JoXWpYB\/a3a2a25cca5bad7f5e83d17519abf503\" src=\"\/\/codepen.io\/anon\/embed\/JoXWpYB\/a3a2a25cca5bad7f5e83d17519abf503?height=450&amp;theme-id=1&amp;slug-hash=JoXWpYB\/a3a2a25cca5bad7f5e83d17519abf503&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed JoXWpYB\/a3a2a25cca5bad7f5e83d17519abf503\" title=\"CodePen Embed JoXWpYB\/a3a2a25cca5bad7f5e83d17519abf503\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>It\u2019s not about moving the world inside of the card, it\u2019s about tricking the flat projection to look 3D by aligning the viewer&#8217;s perspective with the card\u2019s rotation.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-lesson\"><strong>The Lesson<\/strong><\/h2>\n\n\n\n<p>I love this solution not just because it works (and it works beautifully), but because it taught me a humbling lesson.<\/p>\n\n\n\n<p>I had written off <code>perspective-origin<\/code> as a &#8220;bad&#8221; property. I had a mental block against it because I only saw it as a source of distortion. I was so focused on the &#8220;right&#8221; way to do 3D, that I blinded myself to the tools that could actually solve the problem.<\/p>\n\n\n\n<p>Cubiq didn&#8217;t have that bias. He saw a math problem: &#8220;I need the projection to look like X when the rotation is Y.&#8221; And he found the property that controls projection.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"breaking-it-down\">Breaking It Down<\/h2>\n\n\n\n<p>Now that we know it\u2019s possible, let\u2019s break down exactly what\u2019s happening here, step by step, and look at some examples of what you can do with it. Let\u2019s start with the basics.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The HTML<\/h3>\n\n\n\n<p>At its core, the structure is simple. We have a <code>.card-container<\/code> that holds the <code>.card<\/code>, which in turn contains the <code>.card-content<\/code>, that is the &#8216;front&#8217; of the card and where all the inner layers live. and the <code>card-back<\/code> for the back face.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"outer-container\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card-content\"<\/span>&gt;<\/span>\n      <span class=\"hljs-comment\">&lt;!-- Inner layers go here --&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card-back\"<\/span>&gt;<\/span>\n      <span class=\"hljs-comment\">&lt;!-- Back face content --&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Inside the <code>.card-content<\/code>, we can now add <code>.card-layers<\/code> with multiple layers in it. Here I&#8217;m setting a <code>--i<\/code> custom property on each layer to later control its depth.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card-layers\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card-layer\"<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">\"--i: 0\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card-layer\"<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">\"--i: 1\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card-layer\"<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">\"--i: 2\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card-layer\"<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">\"--i: 3\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  <span class=\"hljs-comment\">&lt;!-- more layers as needed --&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Now we can fill each layer with content, images, text, or whatever we want.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Movement<\/h3>\n\n\n\n<p>To create the rotation effect, we need to track the mouse position and convert it into tilt angles for the card. So the first thing we need to do is to map the mouse position into two CSS variables, <code>--mouse-x<\/code> and <code>--mouse-y<\/code>.<\/p>\n\n\n\n<p>This is done with few simple lines of JavaScript:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"JavaScript\" data-shcb-language-slug=\"javascript\"><span><code class=\"hljs language-javascript\"><span class=\"hljs-keyword\">const<\/span> cardContainer = <span class=\"hljs-built_in\">document<\/span>.querySelector(<span class=\"hljs-string\">'.card-container'<\/span>);\n\n<span class=\"hljs-built_in\">window<\/span>.addEventListener(<span class=\"hljs-string\">'mousemove'<\/span>, (e) =&gt; {\n  <span class=\"hljs-keyword\">const<\/span> rect = cardContainer.getBoundingClientRect();\n  <span class=\"hljs-keyword\">const<\/span> x = (e.clientX - rect.left) \/ rect.width * <span class=\"hljs-number\">2<\/span> - <span class=\"hljs-number\">1<\/span>;\n  <span class=\"hljs-keyword\">const<\/span> y = (e.clientY - rect.top) \/ rect.height * <span class=\"hljs-number\">2<\/span> - <span class=\"hljs-number\">1<\/span>;\n  cardContainer.style.setProperty(<span class=\"hljs-string\">'--mouse-x'<\/span>, x);\n  cardContainer.style.setProperty(<span class=\"hljs-string\">'--mouse-y'<\/span>, y);\n});<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">JavaScript<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">javascript<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This gives us normalized values between -1 and 1 on each axis, so we can use them regardless of the card size or aspect ratio.<\/p>\n\n\n\n<p>We convert these values to <code>--tilt-x<\/code> and <code>--tilt-y<\/code> in CSS, by multiplying them by the number of degrees we want the card to rotate:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-6\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-tag\">--tilt-x<\/span>: <span class=\"hljs-selector-tag\">calc<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--mouse-y<\/span>, 0<span class=\"hljs-selector-class\">.1<\/span>) * <span class=\"hljs-selector-tag\">-120deg<\/span>);\n<span class=\"hljs-selector-tag\">--tilt-y<\/span>: <span class=\"hljs-selector-tag\">calc<\/span>(<span class=\"hljs-selector-tag\">var<\/span>(<span class=\"hljs-selector-tag\">--mouse-x<\/span>, 0<span class=\"hljs-selector-class\">.1<\/span>) * 120<span class=\"hljs-selector-tag\">deg<\/span>);<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-6\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>The higher the degree value, the more dramatic the rotation. 20\u201330 degrees will give us a subtle effect, while 180 degrees will spin the card all the way around.<\/p>\n\n\n\n<p class=\"learn-more\">Notice that <code>--mouse-x<\/code> affects <code>--tilt-y<\/code>, because movement of the mouse along the X axis should actually rotate the card around the Y axis, and vice versa. Also, we multiply <code>--mouse-y<\/code> by a negative number, because the Y axis on the screen is inverted compared to the mathematical Y axis.<\/p>\n\n\n\n<p>Now that we have <code>--tilt-x<\/code> and <code>--tilt-y<\/code>, we can start using them. And first, we apply them to the card container to rotate it in 3D space:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-7\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.card<\/span> {\n  <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">rotateX<\/span>(var(--tilt-x)) <span class=\"hljs-built_in\">rotateY<\/span>(var(--tilt-y));\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-7\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This gives us the basic rotation effect. The card will now tilt and spin based on the mouse position.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Perspective<\/h3>\n\n\n\n<p>We need to remember that we need to set <strong>two<\/strong> different perspectives: one for the card&#8217;s container (to create the 3D effect), and one for the card&#8217;s content (to maintain the depth of the inner elements).<\/p>\n\n\n\n<p>on the <code>.card-container<\/code> we set a standard perspective:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-8\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.card-container<\/span> {\n  <span class=\"hljs-attribute\">perspective<\/span>: <span class=\"hljs-built_in\">var<\/span>(--perspective);\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-8\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p class=\"learn-more\">You can set <code>--perspective<\/code> to any value you like, but a good starting point is around <code>800px<\/code>. Lower values will create a more dramatic perspective, while higher values will make it more subtle.<\/p>\n\n\n\n<p>To preserve the 3D space and making sure all the inner elements share the same 3D context, we set <code>transform-style: preserve-3d<\/code>. I&#8217;m using the universal selector here to apply it to all children elements:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\">* {\n  <span class=\"hljs-attribute\">transform-style<\/span>: preserve-<span class=\"hljs-number\">3<\/span>d;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>To deal with the inner perspective, we set up the <code>perspective<\/code> and <code>perspective-origin<\/code> on the <code>.card-content<\/code> element, which holds all the inner layers:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-10\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.card-content<\/span> {\n  <span class=\"hljs-attribute\">perspective<\/span>: <span class=\"hljs-built_in\">calc<\/span>(\n    cos(var(--tilt-x)) * <span class=\"hljs-built_in\">cos<\/span>(var(--tilt-y)) * <span class=\"hljs-built_in\">var<\/span>(--perspective)\n  );\n  <span class=\"hljs-attribute\">perspective-origin<\/span>: \n    <span class=\"hljs-built_in\">calc<\/span>(cos(var(--tilt-x)) * <span class=\"hljs-built_in\">sin<\/span>(var(--tilt-y)) * <span class=\"hljs-built_in\">var<\/span>(--perspective) * -<span class=\"hljs-number\">1<\/span> + <span class=\"hljs-number\">50%<\/span>)\n    <span class=\"hljs-built_in\">calc<\/span>(sin(var(--tilt-x)) * <span class=\"hljs-built_in\">var<\/span>(--perspective) + <span class=\"hljs-number\">50%<\/span>);\n  <span class=\"hljs-attribute\">overflow<\/span>: clip;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-10\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Note that we added <code>overflow: clip<\/code> to the <code>.card-content<\/code> to ensure that the inner elements are clipped by the card boundaries. This combination of <code>perspective<\/code>, <code>perspective-origin<\/code>, and <code>overflow: clip<\/code> is what allows us to maintain the 3D depth of the inner elements while keeping them contained within the card.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Depth<\/h3>\n\n\n\n<p>Now that we have the rotation and perspective set up, we can start adding depth to the inner layers. Each layer will be positioned in 3D space using <code>translateZ<\/code>, based on its <code>--i<\/code> value.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-11\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.card-layer<\/span> {\n  <span class=\"hljs-attribute\">position<\/span>: absolute;\n  <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">translateZ<\/span>(calc(var(--i) * <span class=\"hljs-number\">1rem<\/span>));\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-11\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>This will space out the layers along the Z axis, creating the illusion of depth. You can adjust the multiplier (here <code>1rem<\/code>) to control how far apart the layers are.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"putting-it-all-together\">Putting It All Together<\/h2>\n\n\n\n<p>Using the techniques outlined above, we can create a fully functional Deep Card that responds to mouse movement, maintains 3D depth, and clips its content appropriately.<\/p>\n\n\n\n<p>Here is a complete &#8216;boilerplate&#8217; example:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_QwNmjqo\/3209fa5150d0d591176cbffac7129892\" src=\"\/\/codepen.io\/anon\/embed\/QwNmjqo\/3209fa5150d0d591176cbffac7129892?height=450&amp;theme-id=1&amp;slug-hash=QwNmjqo\/3209fa5150d0d591176cbffac7129892&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed QwNmjqo\/3209fa5150d0d591176cbffac7129892\" title=\"CodePen Embed QwNmjqo\/3209fa5150d0d591176cbffac7129892\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>You can customize it to your needs, set the number of layers, their depth, and add content within each layer to create a wide variety of Deep Card effects.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"getting-deeper\"><strong>Getting Deeper<\/strong><\/h2>\n\n\n\n<p>To improve the Deep Card effect and further enhance the perception of depth, we can add shadows and darkening effects to the layers.<\/p>\n\n\n\n<p>One way to achieve darker colors is just using darker colors. We can calculate the brightness of each layer based on its depth, making deeper layers darker to simulate light falloff.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-12\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.card-layer<\/span> {\n  <span class=\"hljs-attribute\">color<\/span>: <span class=\"hljs-built_in\">hsl<\/span>(<span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">0%<\/span> calc(<span class=\"hljs-number\">100%<\/span> - var(--i) * <span class=\"hljs-number\">9%<\/span>));\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-12\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Another technique is to add semi-transparent background to each layer. This way each layer is like screen that slightly darkens the layers behind it, enhancing the depth effect.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-13\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.card-layer<\/span> {\n  <span class=\"hljs-attribute\">background-color<\/span>: <span class=\"hljs-number\">#2224<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-13\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>Here is an example of a two cards with different effects: The first card uses darker colors for deeper layers, while the second card uses semi-transparent overlays to create a more pronounced depth effect.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_bNpMoKq\/71f99a6de6c2d62e80d7acfc1157da24\" src=\"\/\/codepen.io\/anon\/embed\/bNpMoKq\/71f99a6de6c2d62e80d7acfc1157da24?height=450&amp;theme-id=1&amp;slug-hash=bNpMoKq\/71f99a6de6c2d62e80d7acfc1157da24&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed bNpMoKq\/71f99a6de6c2d62e80d7acfc1157da24\" title=\"CodePen Embed bNpMoKq\/71f99a6de6c2d62e80d7acfc1157da24\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Choose the one that fits your design best, or combine both techniques for an even richer depth experience.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-z-index-effect\"><strong>The <code>z-index<\/code> Effect<\/strong><\/h2>\n\n\n\n<p>You might notice that I\u2019m placing all the layers inside a container (<code>.card-layers<\/code>) rather than making them direct children of <code>.card-content<\/code>. The reason is that since we\u2019re moving the layers along the Z axis, we don\u2019t want them to be direct children of an element with <code>overflow: clip;<\/code> (like <code>.card-content<\/code>).<\/p>\n\n\n\n<p>As mentioned earlier, once you set <code>overflow: clip;<\/code> on <code>.card-content<\/code>, its <code>transform-style<\/code> becomes <code>flat<\/code>, which means all of its direct children are rendered on a single plane. Their stacking order is determined by <code>z-index<\/code>, not by their position along the Z axis. By wrapping the layers in a container, we preserve their 3D positioning and allow the depth effect to work as intended.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">The Twist<\/h3>\n\n\n\n<p>Now that we understand this limitation, let\u2019s turn it to our advantage and see what kinds of effects we can create with it.<\/p>\n\n\n\n<p>Here are the exact same two cards as in the previous example, but this time without a <code>.card-layers<\/code> container. The layers are direct children of <code>.card-content<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_VYaxybX\/060e427f5276054c778606a9b50be906\" src=\"\/\/codepen.io\/anon\/embed\/VYaxybX\/060e427f5276054c778606a9b50be906?height=450&amp;theme-id=1&amp;slug-hash=VYaxybX\/060e427f5276054c778606a9b50be906&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed VYaxybX\/060e427f5276054c778606a9b50be906\" title=\"CodePen Embed VYaxybX\/060e427f5276054c778606a9b50be906\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"adding-interaction\"><strong>Adding Interaction<\/strong><\/h2>\n\n\n\n<p>We often use cards that need to display extra information. One of my favorite things to do in these cases is to rotate the card 180 degrees and reveal the additional content on the back side. Now, we can do exactly that, and build an entire 3D world inside the card.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_pvyLELR\/1d666cd030f54aad4d970aaaf9873046\" src=\"\/\/codepen.io\/anon\/embed\/pvyLELR\/1d666cd030f54aad4d970aaaf9873046?height=450&amp;theme-id=1&amp;slug-hash=pvyLELR\/1d666cd030f54aad4d970aaaf9873046&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed pvyLELR\/1d666cd030f54aad4d970aaaf9873046\" title=\"CodePen Embed pvyLELR\/1d666cd030f54aad4d970aaaf9873046\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>In this example, we have a front face (<code>.card-content<\/code>) and a back face (<code>.card-back<\/code>). When the user clicks the card, we toggle a checkbox that rotates the card 180 degrees, revealing the back face.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-14\" data-shcb-language-name=\"HTML, XML\" data-shcb-language-slug=\"xml\"><span><code class=\"hljs language-xml\"><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card-container\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">input<\/span> <span class=\"hljs-attr\">type<\/span>=<span class=\"hljs-string\">\"checkbox\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card-content\"<\/span>&gt;<\/span>\n      <span class=\"hljs-comment\">&lt;!-- front face content --&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"card-back\"<\/span>&gt;<\/span>\n      <span class=\"hljs-comment\">&lt;!-- back face content --&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span><\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-14\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">HTML, XML<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">xml<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-15\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.card-container<\/span> {\n  <span class=\"hljs-attribute\">cursor<\/span>: pointer;\n    \n  &amp;:has(<span class=\"hljs-attribute\">input<\/span>:checked) .card {\n    rotate: y <span class=\"hljs-number\">180deg<\/span>;\n  }\n  \n  <span class=\"hljs-selector-tag\">input<\/span><span class=\"hljs-selector-attr\">&#91;type=<span class=\"hljs-string\">\"checkbox\"<\/span>]<\/span> {\n    <span class=\"hljs-attribute\">display<\/span>: none;\n  }\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-15\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>You can also use a <code>button<\/code> or any other interactive element to toggle the rotation, depending on your use case, and use any animation technique you like to make the rotation smooth.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"inner-movement\"><strong>Inner Movement<\/strong><\/h2>\n\n\n\n<p>Of course, we can also use any animation on the inner layers to create dynamic effects. It can be wild and complex, or subtle and elegant. The key is that since the layers are in 3D space, any movement along the Z axis will enhance the depth effect.<\/p>\n\n\n\n<p>Here a simple example with parallax layers. each layer animates it&#8217;s background position on the X axis, and to enhance the depth effect, I&#8217;m animating the layers at different speeds based on their depth:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-16\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.card-layer<\/span> {\n  <span class=\"hljs-attribute\">animation<\/span>: layer <span class=\"hljs-built_in\">calc<\/span>(var(--i) * <span class=\"hljs-number\">8s<\/span>) infinite linear;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-16\"><span class=\"shcb-language__label\">Code language:<\/span> <span class=\"shcb-language__name\">CSS<\/span> <span class=\"shcb-language__paren\">(<\/span><span class=\"shcb-language__slug\">css<\/span><span class=\"shcb-language__paren\">)<\/span><\/small><\/pre>\n\n\n<p>And the result:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_bNpMQxj\/5bd4b50fedc6080cfa517d9cf625d81e\" src=\"\/\/codepen.io\/anon\/embed\/bNpMQxj\/5bd4b50fedc6080cfa517d9cf625d81e?height=450&amp;theme-id=1&amp;slug-hash=bNpMQxj\/5bd4b50fedc6080cfa517d9cf625d81e&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed bNpMQxj\/5bd4b50fedc6080cfa517d9cf625d81e\" title=\"CodePen Embed bNpMQxj\/5bd4b50fedc6080cfa517d9cf625d81e\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"deep-text-animation\">Deep Text Animation<\/h2>\n\n\n\n<p>This technique works beautifully with the concept of layered text, opening up a world of creative possibilities. There\u2019s so much you can do with it, from subtle depth effects to wild, animated 3D lettering.<\/p>\n\n\n\n<p>I actually <a href=\"https:\/\/css-tricks.com\/3d-layered-text-motion-and-variations\/\">wrote an entire article about this<\/a>, featuring 20+ examples, and every single one of them looks fantastic inside a Deep Card. Here\u2019s one of the examples from that article, now living inside a card:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_JoXLJQy\/657423f3dbb8bf5781b534b8b79de0c5\" src=\"\/\/codepen.io\/anon\/embed\/JoXLJQy\/657423f3dbb8bf5781b534b8b79de0c5?height=450&amp;theme-id=1&amp;slug-hash=JoXLJQy\/657423f3dbb8bf5781b534b8b79de0c5&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed JoXLJQy\/657423f3dbb8bf5781b534b8b79de0c5\" title=\"CodePen Embed JoXLJQy\/657423f3dbb8bf5781b534b8b79de0c5\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"going-full-360\">Going Full 360<\/h2>\n\n\n\n<p>up until now, we\u2019ve mostly focused on layering our inner content and using the Z axis to create depth. But we can definitely take it a step further, break out of the layering concept, and build a fully 3D object that you can spin around in all directions.<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_QwNrEoP\/9438cf97b6330d2e5f3c30726291c472\" src=\"\/\/codepen.io\/anon\/embed\/QwNrEoP\/9438cf97b6330d2e5f3c30726291c472?height=450&amp;theme-id=1&amp;slug-hash=QwNrEoP\/9438cf97b6330d2e5f3c30726291c472&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed QwNrEoP\/9438cf97b6330d2e5f3c30726291c472\" title=\"CodePen Embed QwNrEoP\/9438cf97b6330d2e5f3c30726291c472\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>From here, the possibilities are truly endless. You can keep experimenting\u2014add more interactions, more layers, or even create effects on both sides of the card to build two complete worlds, one on each face. Or, go all in and design an effect that dives deep into the card itself. The only real limit is your imagination.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"conclusion\">Conclusion<\/h2>\n\n\n\n<p>The <strong>Deep Card<\/strong> is now a solved problem. We can have our cake (3D depth), eat it (clipping), and even spin it around 360 degrees without breaking the illusion.<\/p>\n\n\n\n<p>So, the next time you hit a wall with CSS, and you&#8217;re sure you&#8217;ve tried everything, maybe take a second look at those properties you swore you&#8217;d never use. You might just find your answer hiding in the documentation you skipped.<\/p>\n\n\n\n<p>Now, go build something deep.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>What if you could make a card like a 3D portal, with layers of depth? You probably should just click to see, it&#8217;s a very compelling look.<\/p>\n","protected":false},"author":27,"featured_media":7967,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"sig_custom_text":"","sig_image_type":"featured-image","sig_custom_image":0,"sig_is_disabled":false,"inline_featured_image":false,"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[1],"tags":[103,7],"class_list":["post-7957","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-blog-post","tag-3d","tag-css"],"acf":[],"jetpack_featured_media_url":"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/12\/The-Deep-Card-Conundrum.jpg?fit=1140%2C676&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7957","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/users\/27"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=7957"}],"version-history":[{"count":5,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7957\/revisions"}],"predecessor-version":[{"id":7972,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7957\/revisions\/7972"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/7967"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=7957"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=7957"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=7957"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}