{"id":7802,"date":"2025-11-20T13:25:52","date_gmt":"2025-11-20T18:25:52","guid":{"rendered":"https:\/\/frontendmasters.com\/blog\/?p=7802"},"modified":"2025-11-20T13:25:53","modified_gmt":"2025-11-20T18:25:53","slug":"how-to-create-3d-images-in-css-with-the-layered-pattern","status":"publish","type":"post","link":"https:\/\/frontendmasters.com\/blog\/how-to-create-3d-images-in-css-with-the-layered-pattern\/","title":{"rendered":"How to Create 3D Images in CSS with the Layered Pattern"},"content":{"rendered":"\n<p>3D CSS has been around for a while. The earliest implementation of 3D CSS you can find is in one of W3C\u2019s <a href=\"https:\/\/www.w3.org\/TR\/2009\/WD-css3-3d-transforms-20090320\/\">earliest specifications on 3D transforms<\/a> in 2009. That\u2019s exactly 15 years after <a href=\"https:\/\/www.w3.org\/Style\/CSS20\/history.html\">CSS was introduced to the web in 1994<\/a>, so it\u2019s a really long time!<\/p>\n\n\n\n<p>A common pattern you would see in 3D transformations is the <strong>layered pattern<\/strong>, which gives you the <em>illusion<\/em> of 3D CSS, and this is mostly used with text, like this demo below from Noah Blon:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_ExwmWq\" src=\"\/\/codepen.io\/anon\/embed\/ExwmWq?height=400&amp;theme-id=1&amp;slug-hash=ExwmWq&amp;default-tab=result\" height=\"400\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed ExwmWq\" title=\"CodePen Embed ExwmWq\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Or in <a href=\"https:\/\/codepen.io\/amit_sheen\">Amit Sheen<\/a>\u2019s demos like this one:<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_KwpLrJe\/a41446c5e20cbbdb945beb731d860f63\" src=\"\/\/codepen.io\/anon\/embed\/KwpLrJe\/a41446c5e20cbbdb945beb731d860f63?height=400&amp;theme-id=1&amp;slug-hash=KwpLrJe\/a41446c5e20cbbdb945beb731d860f63&amp;default-tab=result\" height=\"400\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed KwpLrJe\/a41446c5e20cbbdb945beb731d860f63\" title=\"CodePen Embed KwpLrJe\/a41446c5e20cbbdb945beb731d860f63\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>The layered pattern, as its name suggests, stacks multiple items into layers, adjusting the Z position and colors of each item with respect to their index value in order to create an illusion of 3D.<\/p>\n\n\n\n<p><strong>Yes, most 3D CSS are just illusions.<\/strong> However, did you know that we can apply the same pattern, but for images? In this article, we will look into how to create a layered pattern for images to create a 3D image in CSS.<\/p>\n\n\n\n<p>In order for you to truly understand how 3D CSS works, here\u2019s a quick list of things you need to do before proceeding:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><a href=\"https:\/\/css-tricks.com\/how-css-perspective-works\/\">How the CSS perspective works<\/a><\/li>\n\n\n\n<li>A good understanding of <a href=\"https:\/\/byjus.com\/maths\/co-ordinates-of-a-point-in-three-dimensions\/\">the x, y, and z coordinates<\/a><\/li>\n\n\n\n<li><a href=\"https:\/\/css-tricks.com\/css-in-3d-learning-to-think-in-cubes-instead-of-boxes\/\">Sometimes, you have to think in cubes<\/a> (bonus)<\/li>\n<\/ol>\n\n\n\n<p class=\"learn-more\">This layered pattern can be an accessibility problem because duplicated content is read as many times as its repeated. That\u2019s true for text, however, for images this can be circumvented by just leaving all the but first&nbsp;<code>alt<\/code>&nbsp;attribute empty or setting all the duplicated divs with&nbsp;<code>aria-hidden=\"true\"<\/code>&nbsp;(this one also works for text). This would hide the duplicated content from the user.<\/p>\n\n\n\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"the-main-sauce\">The HTML<\/h2>\n\n\n\n<p>Let\u2019s start with the basic markup structure. We\u2019re linking up an identical&nbsp;<code>&lt;img&gt;<\/code>&nbsp;over and over in multiple layers:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-1\" 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\">\"scene\"<\/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\">\"image-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\">\"original\"<\/span>&gt;<\/span>\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"https:\/\/images.unsplash.com\/photo-1579546929518-9e396f3cc809\"<\/span> <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"Gradient colored image with all colors present starting from the center point\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n    \n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"layers\"<\/span> <span class=\"hljs-attr\">aria-hidden<\/span>=<span class=\"hljs-string\">\"true\"<\/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\">\"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\">img<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"https:\/\/images.unsplash.com\/photo-1579546929518-9e396f3cc809\"<\/span> <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"\"<\/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\">\"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\">img<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"https:\/\/images.unsplash.com\/photo-1579546929518-9e396f3cc809\"<\/span> <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"\"<\/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\">\"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\">img<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"https:\/\/images.unsplash.com\/photo-1579546929518-9e396f3cc809\"<\/span> <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"\"<\/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\">\"layer\"<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">\"--i: 4;\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"https:\/\/images.unsplash.com\/photo-1579546929518-9e396f3cc809\"<\/span> <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"\"<\/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\">\"layer\"<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">\"--i: 5;\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"https:\/\/images.unsplash.com\/photo-1579546929518-9e396f3cc809\"<\/span> <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">div<\/span>&gt;<\/span>\n      ...\n      <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"layer\"<\/span> <span class=\"hljs-attr\">style<\/span>=<span class=\"hljs-string\">\"--i: 35;\"<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">img<\/span> <span class=\"hljs-attr\">src<\/span>=<span class=\"hljs-string\">\"https:\/\/images.unsplash.com\/photo-1579546929518-9e396f3cc809\"<\/span> <span class=\"hljs-attr\">alt<\/span>=<span class=\"hljs-string\">\"\"<\/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>&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-1\"><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>The first&nbsp;<code>&lt;div&gt;<\/code>&nbsp;has a \u201cscene\u201d class wrapped around all the layers. Each layer&nbsp;<code>&lt;div&gt;<\/code>&nbsp;has an&nbsp;<strong>index<\/strong>&nbsp;custom property set&nbsp;<code>--i<\/code>&nbsp;in the&nbsp;<code>style<\/code>&nbsp;attribute. This index value is very important, as we will use it later to calculate positioning values. Notice how the&nbsp;<code>&lt;div&gt;<\/code>&nbsp;with class \u201coriginal\u201d doesn\u2019t have the <code>aria-hidden <\/code>attribute? That\u2019s because we want the screen reader to read that first image and not the rest.<\/p>\n\n\n\n<p class=\"learn-more\">We\u2019re using the&nbsp;<code>style<\/code>&nbsp;indexing approach and not&nbsp;<code>sibling-index()<\/code>&nbsp;\/ <code>sibling-count()<\/code> because they are <a href=\"https:\/\/developer.mozilla.org\/en-US\/docs\/Web\/CSS\/Reference\/Values\/sibling-index#browser_compatibility\">not yet supported globally across all major browsers<\/a>. In the future with better support, we could remove the inline styles and use <code>sibling-index()<\/code> wherever we&#8217;re using <code>--i<\/code> in calculations and <code>sibling-count()<\/code> when you need to total (35 in this blog post). <\/p>\n\n\n\n<p>It\u2019s important we start with a container for our scene as well because we will apply the CSS&nbsp;<code>perspective<\/code>&nbsp;property, which controls the <em>depth<\/em> of our 3D element.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"css\">The CSS<\/h2>\n\n\n\n<p>Setting the scene, we use a&nbsp;<code>1000px<\/code>&nbsp;value for the perspective. A large perspective value is typically good, so the 3D element won\u2019t be too close to the user, but feel free to still use any perspective value of your choice. <\/p>\n\n\n\n<p>We then set all the elements, including the image container&nbsp;<code>&lt;div&gt;<\/code>s to have a&nbsp;<code>transform-style<\/code>&nbsp;of&nbsp;<code>preserve-3d<\/code>. This allows the stacked items to be visible in the 3D space.<\/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\">.scene<\/span> {\n  <span class=\"hljs-attribute\">perspective<\/span>: <span class=\"hljs-number\">1000px<\/span>;\n}\n\n<span class=\"hljs-selector-class\">.scene<\/span> * {\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-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<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_RNrdBYa\" src=\"\/\/codepen.io\/anon\/embed\/RNrdBYa?height=450&amp;theme-id=1&amp;slug-hash=RNrdBYa&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed RNrdBYa\" title=\"CodePen Embed RNrdBYa\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Everything looks a little janky, but that\u2019s expected until we add a bit more CSS to make it look cool.<\/p>\n\n\n\n<p>We need to calculate the offset distance between each of the stacked layers, that is, the distance each layer will have against each other in order for it to appear together or completely separated.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img data-recalc-dims=\"1\" loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"768\" src=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/2TvXFOKc.png?resize=1024%2C768&#038;ssl=1\" alt=\"Illustration of layered blocks showing layer offsets in a 3D perspective with a gradient background.\" class=\"wp-image-7807\" srcset=\"https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/2TvXFOKc.png?resize=1024%2C768&amp;ssl=1 1024w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/2TvXFOKc.png?resize=300%2C225&amp;ssl=1 300w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/2TvXFOKc.png?resize=768%2C576&amp;ssl=1 768w, https:\/\/i0.wp.com\/frontendmasters.com\/blog\/wp-content\/uploads\/2025\/11\/2TvXFOKc.png?w=1200&amp;ssl=1 1200w\" sizes=\"auto, (max-width: 1000px) 100vw, 1000px\" \/><\/figure>\n\n\n\n<p>On the image container, we set two variables: the offset distance to be just <code>2px<\/code> and the total layers. These would be used to calculate the offset on the Z-axis and the colors between them to make it appear as a single whole 3D element.<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-3\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.image-container<\/span>{\n  ...\n  <span class=\"hljs-attribute\">--layers-count<\/span>: <span class=\"hljs-number\">35<\/span>;\n  <span class=\"hljs-attribute\">--layer-offset<\/span>: <span class=\"hljs-number\">2.5px<\/span>;\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-3\"><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>That\u2019s not all, we now calculate the distance between each layer using the index&nbsp;<code>--i<\/code>&nbsp;and the offset on the&nbsp;<code>translateZ()<\/code>&nbsp;function inside the layer class:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-4\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.layer<\/span> {\n  <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">translateZ<\/span>(calc(var(--i) * <span class=\"hljs-built_in\">var<\/span>(--layer-offset)));\n  ...\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-4\"><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 next step is to use a normalized value (because the index would be too big) to calculate how dark and saturated we want each image to be, so it appears darker in 3D as it goes down in index value. i.e:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-5\" data-shcb-language-name=\"CSS\" data-shcb-language-slug=\"css\"><span><code class=\"hljs language-css\"><span class=\"hljs-selector-class\">.layer<\/span> {\n  ...\n  <span class=\"hljs-attribute\">--n<\/span>: <span class=\"hljs-built_in\">calc<\/span>(var(--i) \/ <span class=\"hljs-built_in\">var<\/span>(--layers-count));\n  <span class=\"hljs-attribute\">filter<\/span>: \n    <span class=\"hljs-built_in\">brightness<\/span>(calc(<span class=\"hljs-number\">0.4<\/span> + var(--n) * <span class=\"hljs-number\">0.8<\/span>))\n    <span class=\"hljs-built_in\">saturate<\/span>(calc(<span class=\"hljs-number\">0.8<\/span> + var(--n) * <span class=\"hljs-number\">0.4<\/span>));\n}<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-5\"><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>I\u2019m adding <code>0.4<\/code> to the multiplied value of 80% and <code>--n<\/code>. If <code>--n<\/code> is <code>2\/35<\/code> for example, our brightness value would equal to <code>0.45<\/code> (<code>0.4<\/code> + <code>2\/36<\/code> x <code>0.8<\/code>) and the saturation would be equal to <code>0.83<\/code>. If <code>--``n<\/code> is <code>3\/35<\/code>, the brightness value would be <code>0.47<\/code>, while the saturation would be <code>0.82<\/code> and so on.<\/p>\n\n\n\n<p>And that\u2019s it! We\u2019re all set! (sike! Not yet).<\/p>\n\n\n\n<p>We just need to set the <code>position<\/code> property to <code>absolute<\/code> and <code>inset<\/code> to be <code>0<\/code> for all the layers so they can be on top of each other. Don\u2019t forget to set the height and width to any desired length, and the <code>position<\/code> property of the <code>image-container<\/code> class to <code>relative<\/code> while you\u2019re at it. Here\u2019s the code if you\u2019ve been following:<\/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-class\">.image-container<\/span> {\n  <span class=\"hljs-attribute\">position<\/span>: relative;\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">300px<\/span>;\n  <span class=\"hljs-attribute\">height<\/span>: <span class=\"hljs-number\">300px<\/span>;\n  <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">rotateX<\/span>(<span class=\"hljs-number\">20deg<\/span>) <span class=\"hljs-built_in\">rotateY<\/span>(-<span class=\"hljs-number\">10deg<\/span>);\n  <span class=\"hljs-attribute\">--layers-count<\/span>: <span class=\"hljs-number\">35<\/span>;\n  <span class=\"hljs-attribute\">--layer-offset<\/span>: <span class=\"hljs-number\">2.5px<\/span>;\n}\n\n<span class=\"hljs-selector-class\">.layers<\/span>,\n<span class=\"hljs-selector-class\">.layer<\/span> {\n  <span class=\"hljs-attribute\">position<\/span>: absolute;\n  <span class=\"hljs-attribute\">inset<\/span>: <span class=\"hljs-number\">0<\/span>;\n}\n\n<span class=\"hljs-selector-class\">.layer<\/span> {\n  <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">translateZ<\/span>(calc(var(--i) * <span class=\"hljs-built_in\">var<\/span>(--layer-offset)));\n  <span class=\"hljs-attribute\">--n<\/span>: <span class=\"hljs-built_in\">calc<\/span>(var(--i) \/ <span class=\"hljs-built_in\">var<\/span>(--layers-count));\n  <span class=\"hljs-attribute\">filter<\/span>: \n    <span class=\"hljs-built_in\">brightness<\/span>(calc(<span class=\"hljs-number\">0.4<\/span> + var(--n) * <span class=\"hljs-number\">0.8<\/span>))\n    <span class=\"hljs-built_in\">saturate<\/span>(calc(<span class=\"hljs-number\">0.8<\/span> + var(--n) * <span class=\"hljs-number\">0.4<\/span>));\n}<\/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>Here\u2019s a quick breakdown of the mathematical calculations going on:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><code>translateZ()<\/code>&nbsp;makes the items stacked visible by calculating them based on their index multiplied by&nbsp;<code>--layer-offset<\/code>. This moves it away from the user, which is our main 3D affect here.<\/li>\n\n\n\n<li><code>--n<\/code> is used to normalize the index to a 0-1 range<\/li>\n\n\n\n<li><code>filter<\/code> is then used with <code>--n<\/code> to calculate the saturation and brightness of the 3D element<\/li>\n<\/ul>\n\n\n\n<p>That\u2019s actually where most of the logic lies. This next part is just basic sizing, positioning, and polish.<\/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\">.layer<\/span> <span class=\"hljs-selector-tag\">img<\/span> {\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">100%<\/span>;\n  <span class=\"hljs-attribute\">height<\/span>: <span class=\"hljs-number\">100%<\/span>;\n  <span class=\"hljs-attribute\">object-fit<\/span>: cover;\n  <span class=\"hljs-attribute\">border-radius<\/span>: <span class=\"hljs-number\">20px<\/span>;\n  <span class=\"hljs-attribute\">display<\/span>: block;\n}\n\n<span class=\"hljs-selector-class\">.original<\/span> {\n  <span class=\"hljs-attribute\">position<\/span>: relative;\n  <span class=\"hljs-attribute\">z-index<\/span>: <span class=\"hljs-number\">1<\/span>;\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">18.75rem<\/span>;\n  <span class=\"hljs-attribute\">height<\/span>: <span class=\"hljs-number\">18.75rem<\/span>;\n}\n\n<span class=\"hljs-selector-class\">.original<\/span> <span class=\"hljs-selector-tag\">img<\/span> {\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">100%<\/span>;\n  <span class=\"hljs-attribute\">height<\/span>: <span class=\"hljs-number\">100%<\/span>;\n  <span class=\"hljs-attribute\">object-fit<\/span>: cover;\n  <span class=\"hljs-attribute\">border-radius<\/span>: <span class=\"hljs-number\">20px<\/span>;\n  <span class=\"hljs-attribute\">display<\/span>: block;\n  <span class=\"hljs-attribute\">box-shadow<\/span>: <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">20px<\/span> <span class=\"hljs-number\">60px<\/span> <span class=\"hljs-built_in\">rgba<\/span>(<span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">0<\/span> \/ <span class=\"hljs-number\">0.6<\/span>);\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>Check out the final result. Doesn\u2019t it look so cool?!<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_azdPmBJ\" src=\"\/\/codepen.io\/anon\/embed\/azdPmBJ?height=450&amp;theme-id=1&amp;slug-hash=azdPmBJ&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed azdPmBJ\" title=\"CodePen Embed azdPmBJ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\"><strong>We\u2019re not done yet!<\/strong><\/h2>\n\n\n\n<p>Who\u2019s ready for a little bit more&nbsp;<em>interactivity<\/em>? \ud83d\ude4b\ud83c\udffe I know I am. Let\u2019s add a rotation animation to emphasize the 3D affet.<\/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 shcb-code-table\"><span class='shcb-loc'><span><span class=\"hljs-selector-class\">.image-container<\/span> {\n<\/span><\/span><span class='shcb-loc'><span>  ...\n<\/span><\/span><mark class='shcb-loc'><span>  <span class=\"hljs-attribute\">animation<\/span>: rotate3d <span class=\"hljs-number\">8s<\/span> ease-in-out infinite alternate; \n<\/span><\/mark><span class='shcb-loc'><span>}\n<\/span><\/span><span class='shcb-loc'><span>\n<\/span><\/span><span class='shcb-loc'><span><span class=\"hljs-keyword\">@keyframes<\/span> rotate3d {\n<\/span><\/span><span class='shcb-loc'><span>  0% {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">rotateX<\/span>(-<span class=\"hljs-number\">20deg<\/span>) <span class=\"hljs-built_in\">rotateY<\/span>(<span class=\"hljs-number\">30deg<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>  100% {\n<\/span><\/span><span class='shcb-loc'><span>    <span class=\"hljs-attribute\">transform<\/span>: <span class=\"hljs-built_in\">rotateX<\/span>(-<span class=\"hljs-number\">15deg<\/span>) <span class=\"hljs-built_in\">rotateY<\/span>(-<span class=\"hljs-number\">40deg<\/span>);\n<\/span><\/span><span class='shcb-loc'><span>  }\n<\/span><\/span><span class='shcb-loc'><span>}\n<\/span><\/span><\/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>Our final result looks like this! Isn\u2019t this so cool?<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_KwVYjQq\" src=\"\/\/codepen.io\/anon\/embed\/KwVYjQq?height=550&amp;theme-id=1&amp;slug-hash=KwVYjQq&amp;default-tab=result\" height=\"550\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed KwVYjQq\" title=\"CodePen Embed KwVYjQq\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"bonus\">Bonus: Adding a control feature<\/h2>\n\n\n\n<p>Remember how this article is about images and not gradients? Although the image used was an image of a gradient, I\u2019d like to take things a step further by being able to control things like <code>perspective<\/code>, layer offset, and its rotation. The bonus step is adding a <em>form of controls<\/em>.<\/p>\n\n\n\n<p>We first need to add the boilerplate HTML and styling for the controls:<\/p>\n\n\n<pre class=\"wp-block-code\" aria-describedby=\"shcb-language-9\" 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\">\"controls\"<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">h3<\/span>&gt;<\/span>3D Controls<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">h3<\/span>&gt;<\/span>\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span>&gt;<\/span>Perspective: <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">span<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"perspValue\"<\/span>&gt;<\/span>1000px<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">span<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/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\">\"range\"<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"perspective\"<\/span> <span class=\"hljs-attr\">min<\/span>=<span class=\"hljs-string\">\"200\"<\/span> <span class=\"hljs-attr\">max<\/span>=<span class=\"hljs-string\">\"2000\"<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">\"1000\"<\/span>&gt;<\/span>\n\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span>&gt;<\/span>Layer Offset: <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">span<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"offsetValue\"<\/span>&gt;<\/span>2px<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">span<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/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\">\"range\"<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"offset\"<\/span> <span class=\"hljs-attr\">min<\/span>=<span class=\"hljs-string\">\"0.5\"<\/span> <span class=\"hljs-attr\">max<\/span>=<span class=\"hljs-string\">\"5\"<\/span> <span class=\"hljs-attr\">step<\/span>=<span class=\"hljs-string\">\"0.1\"<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">\"2\"<\/span>&gt;<\/span>\n\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span>&gt;<\/span>Rotate X: <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">span<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"rotXValue\"<\/span>&gt;<\/span>20\u00b0<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">span<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/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\">\"range\"<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"rotateX\"<\/span> <span class=\"hljs-attr\">min<\/span>=<span class=\"hljs-string\">\"-90\"<\/span> <span class=\"hljs-attr\">max<\/span>=<span class=\"hljs-string\">\"90\"<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">\"20\"<\/span>&gt;<\/span>\n\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span>&gt;<\/span>Rotate Y: <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">span<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"rotYValue\"<\/span>&gt;<\/span>-10\u00b0<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">span<\/span>&gt;<\/span><span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/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\">\"range\"<\/span> <span class=\"hljs-attr\">id<\/span>=<span class=\"hljs-string\">\"rotateY\"<\/span> <span class=\"hljs-attr\">min<\/span>=<span class=\"hljs-string\">\"-90\"<\/span> <span class=\"hljs-attr\">max<\/span>=<span class=\"hljs-string\">\"90\"<\/span> <span class=\"hljs-attr\">value<\/span>=<span class=\"hljs-string\">\"-10\"<\/span>&gt;<\/span>\n\n  <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">div<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"image-selector\"<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">label<\/span>&gt;<\/span>Try Different Images:<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">label<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">data-img<\/span>=<span class=\"hljs-string\">\"https:\/\/images.unsplash.com\/photo-1579546929518-9e396f3cc809\"<\/span> <span class=\"hljs-attr\">class<\/span>=<span class=\"hljs-string\">\"active\"<\/span>&gt;<\/span>Abstract Gradient<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">data-img<\/span>=<span class=\"hljs-string\">\"https:\/\/images.unsplash.com\/photo-1506905925346-21bda4d32df4\"<\/span>&gt;<\/span>Mountain Landscape<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">data-img<\/span>=<span class=\"hljs-string\">\"https:\/\/images.unsplash.com\/photo-1518791841217-8f162f1e1131\"<\/span>&gt;<\/span>Cat Portrait<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/span>&gt;<\/span>\n    <span class=\"hljs-tag\">&lt;<span class=\"hljs-name\">button<\/span> <span class=\"hljs-attr\">data-img<\/span>=<span class=\"hljs-string\">\"https:\/\/images.unsplash.com\/photo-1470071459604-3b5ec3a7fe05\"<\/span>&gt;<\/span>Foggy Forest<span class=\"hljs-tag\">&lt;\/<span class=\"hljs-name\">button<\/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>\n<\/code><\/span><small class=\"shcb-language\" id=\"shcb-language-9\"><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>This would give us access to a host of images to select from, and we would also be able to rotate the main 3D element as we please using&nbsp;<code>&lt;input&gt;<\/code>&nbsp;type&nbsp;<code>range<\/code>&nbsp;and&nbsp;<code>&lt;button&gt;<\/code>s.<\/p>\n\n\n\n<p>The CSS is to add basic styles to the form controls. Nothing too complicated:<\/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\">.controls<\/span> {\n  <span class=\"hljs-attribute\">display<\/span>: flex;\n  <span class=\"hljs-attribute\">flex-direction<\/span>: column;\n  <span class=\"hljs-attribute\">justify-content<\/span>: space-between;\n  <span class=\"hljs-attribute\">position<\/span>: absolute;\n  <span class=\"hljs-attribute\">top<\/span>: <span class=\"hljs-number\">1.2rem<\/span>;\n  <span class=\"hljs-attribute\">right<\/span>: <span class=\"hljs-number\">1.2rem<\/span>;\n  <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">rgba<\/span>(<span class=\"hljs-number\">255<\/span>, <span class=\"hljs-number\">255<\/span>, <span class=\"hljs-number\">255<\/span>, <span class=\"hljs-number\">0.1<\/span>);\n  <span class=\"hljs-attribute\">backdrop-filter<\/span>: <span class=\"hljs-built_in\">blur<\/span>(<span class=\"hljs-number\">10px<\/span>);\n  <span class=\"hljs-attribute\">padding<\/span>: <span class=\"hljs-number\">1.15rem<\/span>;\n  <span class=\"hljs-attribute\">height<\/span>: <span class=\"hljs-number\">20rem<\/span>;\n  <span class=\"hljs-attribute\">border-radius<\/span>: <span class=\"hljs-number\">10px<\/span>;\n  <span class=\"hljs-attribute\">overflow-y<\/span>: scroll;\n  <span class=\"hljs-attribute\">color<\/span>: white;\n  <span class=\"hljs-attribute\">max-width<\/span>: <span class=\"hljs-number\">250px<\/span>;\n}\n\n<span class=\"hljs-selector-class\">.controls<\/span> <span class=\"hljs-selector-tag\">h3<\/span> {\n  <span class=\"hljs-attribute\">margin-bottom<\/span>: <span class=\"hljs-number\">15px<\/span>;\n  <span class=\"hljs-attribute\">font-size<\/span>: <span class=\"hljs-number\">1.15rem<\/span>;\n}\n\n<span class=\"hljs-selector-class\">.controls<\/span> <span class=\"hljs-selector-tag\">label<\/span> {\n  <span class=\"hljs-attribute\">display<\/span>: flex;\n  <span class=\"hljs-attribute\">justify-content<\/span>: space-between;\n  <span class=\"hljs-attribute\">gap<\/span>: <span class=\"hljs-number\">0.5rem<\/span>;\n  <span class=\"hljs-attribute\">margin<\/span>: <span class=\"hljs-number\">15px<\/span> <span class=\"hljs-number\">0<\/span> <span class=\"hljs-number\">5px<\/span>;\n  <span class=\"hljs-attribute\">font-size<\/span>: <span class=\"hljs-number\">0.8125rem<\/span>;\n  <span class=\"hljs-attribute\">font-weight<\/span>: <span class=\"hljs-number\">500<\/span>;\n}\n\n<span class=\"hljs-selector-class\">.controls<\/span> <span class=\"hljs-selector-tag\">input<\/span> {\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">100%<\/span>;\n}\n\n<span class=\"hljs-selector-class\">.controls<\/span> <span class=\"hljs-selector-tag\">span<\/span> {\n  <span class=\"hljs-attribute\">font-weight<\/span>: bold;\n}\n\n<span class=\"hljs-selector-class\">.image-selector<\/span> {\n  <span class=\"hljs-attribute\">margin-top<\/span>: <span class=\"hljs-number\">20px<\/span>;\n  <span class=\"hljs-attribute\">padding-top<\/span>: <span class=\"hljs-number\">20px<\/span>;\n  <span class=\"hljs-attribute\">border-top<\/span>: <span class=\"hljs-number\">1px<\/span> solid <span class=\"hljs-built_in\">rgb<\/span>(<span class=\"hljs-number\">255<\/span> <span class=\"hljs-number\">255<\/span> <span class=\"hljs-number\">255<\/span> \/ <span class=\"hljs-number\">0.2<\/span>);\n}\n\n<span class=\"hljs-selector-class\">.image-selector<\/span> <span class=\"hljs-selector-tag\">button<\/span> {\n  <span class=\"hljs-attribute\">width<\/span>: <span class=\"hljs-number\">100%<\/span>;\n  <span class=\"hljs-attribute\">padding<\/span>: <span class=\"hljs-number\">8px<\/span>;\n  <span class=\"hljs-attribute\">margin<\/span>: <span class=\"hljs-number\">5px<\/span> <span class=\"hljs-number\">0<\/span>;\n  <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">rgb<\/span>(<span class=\"hljs-number\">255<\/span> <span class=\"hljs-number\">255<\/span> <span class=\"hljs-number\">255<\/span> \/ <span class=\"hljs-number\">0.2<\/span>);\n  <span class=\"hljs-attribute\">border<\/span>: <span class=\"hljs-number\">1px<\/span> solid <span class=\"hljs-built_in\">rgb<\/span>(<span class=\"hljs-number\">255<\/span> <span class=\"hljs-number\">255<\/span> <span class=\"hljs-number\">255<\/span> \/ <span class=\"hljs-number\">0.3<\/span>);\n  <span class=\"hljs-attribute\">border-radius<\/span>: <span class=\"hljs-number\">5px<\/span>;\n  <span class=\"hljs-attribute\">color<\/span>: white;\n  <span class=\"hljs-attribute\">cursor<\/span>: pointer;\n  <span class=\"hljs-attribute\">font-size<\/span>: <span class=\"hljs-number\">12px<\/span>;\n  <span class=\"hljs-attribute\">transition<\/span>: all <span class=\"hljs-number\">0.3s<\/span>;\n}\n\n<span class=\"hljs-selector-class\">.image-selector<\/span> <span class=\"hljs-selector-tag\">button<\/span><span class=\"hljs-selector-pseudo\">:hover<\/span> {\n  <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">rgb<\/span>(<span class=\"hljs-number\">255<\/span> <span class=\"hljs-number\">255<\/span> <span class=\"hljs-number\">255<\/span> \/ <span class=\"hljs-number\">0.3<\/span>);\n}\n\n<span class=\"hljs-selector-class\">.image-selector<\/span> <span class=\"hljs-selector-tag\">button<\/span><span class=\"hljs-selector-class\">.active<\/span> {\n  <span class=\"hljs-attribute\">background<\/span>: <span class=\"hljs-built_in\">rgb<\/span>(<span class=\"hljs-number\">255<\/span> <span class=\"hljs-number\">255<\/span> <span class=\"hljs-number\">255<\/span> \/ <span class=\"hljs-number\">0.4<\/span>);\n  <span class=\"hljs-attribute\">border-color<\/span>: white;\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><a href=\"https:\/\/codepen.io\/sunkanmii-the-styleful\/pen\/wBMZVBd\">This creates the controls<\/a> like we want. We haven\u2019t finished, though. Try making some adjustments, and you\u2019d notice that it doesn&#8217;t do anything. Why? Because we haven\u2019t applied any JS!<\/p>\n\n\n\n<p>The code below would affect the rotation values on the x and y axis, layer offset, and perspective. It would also change the images to any of the other 3 specified:<\/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-tag\">const<\/span> <span class=\"hljs-selector-tag\">scene<\/span> = <span class=\"hljs-selector-tag\">document<\/span><span class=\"hljs-selector-class\">.querySelector<\/span>(\"<span class=\"hljs-selector-class\">.scene<\/span>\");\n<span class=\"hljs-selector-tag\">const<\/span> <span class=\"hljs-selector-tag\">container<\/span> = <span class=\"hljs-selector-tag\">document<\/span><span class=\"hljs-selector-class\">.querySelector<\/span>(\"<span class=\"hljs-selector-class\">.image-container<\/span>\");\n\n<span class=\"hljs-selector-tag\">document<\/span><span class=\"hljs-selector-class\">.getElementById<\/span>(\"<span class=\"hljs-selector-tag\">perspective<\/span>\")<span class=\"hljs-selector-class\">.addEventListener<\/span>(\"<span class=\"hljs-selector-tag\">input<\/span>\", (<span class=\"hljs-selector-tag\">e<\/span>) =&gt; {\n  const val = e.target.value;\n  scene.style.perspective = val + \"px\";\n  document.getElementById(\"perspValue\").textContent = val + \"px\";\n});\n\n<span class=\"hljs-selector-tag\">document<\/span><span class=\"hljs-selector-class\">.getElementById<\/span>(\"<span class=\"hljs-selector-tag\">offset<\/span>\")<span class=\"hljs-selector-class\">.addEventListener<\/span>(\"<span class=\"hljs-selector-tag\">input<\/span>\", (<span class=\"hljs-selector-tag\">e<\/span>) =&gt; {\n  const val = e.target.value;\n  container.style.setProperty(\"--layer-offset\", val + \"px\");\n  document.getElementById(\"offsetValue\").textContent = val + \"px\";\n});\n\n<span class=\"hljs-selector-tag\">document<\/span><span class=\"hljs-selector-class\">.getElementById<\/span>(\"<span class=\"hljs-selector-tag\">rotateX<\/span>\")<span class=\"hljs-selector-class\">.addEventListener<\/span>(\"<span class=\"hljs-selector-tag\">input<\/span>\", (<span class=\"hljs-selector-tag\">e<\/span>) =&gt; {\n  const val = e.target.value;\n  updateRotation();\n  document.getElementById(\"rotXValue\").textContent = val + \"\u00b0\";\n});\n\n<span class=\"hljs-selector-tag\">document<\/span><span class=\"hljs-selector-class\">.getElementById<\/span>(\"<span class=\"hljs-selector-tag\">rotateY<\/span>\")<span class=\"hljs-selector-class\">.addEventListener<\/span>(\"<span class=\"hljs-selector-tag\">input<\/span>\", (<span class=\"hljs-selector-tag\">e<\/span>) =&gt; {\n  const val = e.target.value;\n  updateRotation();\n  document.getElementById(\"rotYValue\").textContent = val + \"\u00b0\";\n});\n\n<span class=\"hljs-selector-tag\">function<\/span> <span class=\"hljs-selector-tag\">updateRotation<\/span>() {\n  const x = document.getElementById(\"rotateX\").value;\n  const y = document.getElementById(\"rotateY\").value;\n  container.style.transform = `rotateX(${x}<span class=\"hljs-selector-tag\">deg<\/span>) <span class=\"hljs-selector-tag\">rotateY<\/span>(${y}<span class=\"hljs-selector-tag\">deg<\/span>)`;\n}\n\n\/\/ <span class=\"hljs-selector-tag\">Image<\/span> <span class=\"hljs-selector-tag\">selector<\/span>\n<span class=\"hljs-selector-tag\">document<\/span><span class=\"hljs-selector-class\">.querySelectorAll<\/span>(\"<span class=\"hljs-selector-class\">.image-selector<\/span> <span class=\"hljs-selector-tag\">button<\/span>\")<span class=\"hljs-selector-class\">.forEach<\/span>((<span class=\"hljs-selector-tag\">btn<\/span>) =&gt; {\n  btn.addEventListener(\"click\", () =&gt; {\n    const imgUrl = btn.dataset.img;\n\n    \/\/ Update all images\n    document.querySelectorAll(\"img\").forEach((img) =&gt; {\n      img.src = imgUrl;\n    });\n\n    \/\/ <span class=\"hljs-selector-tag\">Update<\/span> <span class=\"hljs-selector-tag\">active<\/span> <span class=\"hljs-selector-tag\">button<\/span>\n    <span class=\"hljs-selector-tag\">document<\/span>\n      <span class=\"hljs-selector-class\">.querySelectorAll<\/span>(\"<span class=\"hljs-selector-class\">.image-selector<\/span> <span class=\"hljs-selector-tag\">button<\/span>\")\n      <span class=\"hljs-selector-class\">.forEach<\/span>((<span class=\"hljs-selector-tag\">b<\/span>) =&gt; <span class=\"hljs-selector-tag\">b<\/span><span class=\"hljs-selector-class\">.classList<\/span><span class=\"hljs-selector-class\">.remove<\/span>(\"<span class=\"hljs-selector-tag\">active<\/span>\"));\n    <span class=\"hljs-selector-tag\">btn<\/span><span class=\"hljs-selector-class\">.classList<\/span><span class=\"hljs-selector-class\">.add<\/span>(\"<span class=\"hljs-selector-tag\">active<\/span>\");\n  });\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>Plus we pop into the CSS and remove the animation, as we can control it ourselves now. Viola! We have a full working demo with various form controls and an image change feature. Go on, change the image to something else to view the result. <\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_vELMoMZ\" src=\"\/\/codepen.io\/anon\/embed\/vELMoMZ?height=650&amp;theme-id=1&amp;slug-hash=vELMoMZ&amp;default-tab=result\" height=\"650\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed vELMoMZ\" title=\"CodePen Embed vELMoMZ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"3d-css-steak\">Bonus: 3D CSS&#8230; Steak<\/h2>\n\n\n\n<p>Using this same technique, you know what else we can build? a 3D CSS steak!<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_ByjexvJ\" src=\"\/\/codepen.io\/anon\/embed\/ByjexvJ?height=450&amp;theme-id=1&amp;slug-hash=ByjexvJ&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed ByjexvJ\" title=\"CodePen Embed ByjexvJ\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>It\u2019s currently in black &amp; white. Let\u2019s make it show some color, shall we?<\/p>\n\n\n\n<div class=\"wp-block-cp-codepen-gutenberg-embed-block cp_embed_wrapper\"><iframe id=\"cp_embed_azdrGMz\" src=\"\/\/codepen.io\/anon\/embed\/azdrGMz?height=450&amp;theme-id=1&amp;slug-hash=azdrGMz&amp;default-tab=result\" height=\"450\" scrolling=\"no\" frameborder=\"0\" allowfullscreen allowpaymentrequest name=\"CodePen Embed azdrGMz\" title=\"CodePen Embed azdrGMz\" class=\"cp_embed_iframe\" style=\"width:100%;overflow:hidden\">CodePen Embed Fallback<\/iframe><\/div>\n\n\n\n<p>Summary of things I\u2019m doing to make this work:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Create a scene, adding the CSS <code>perspective<\/code> property<\/li>\n\n\n\n<li>Duplicate a single image into separate containers<\/li>\n\n\n\n<li>Apply transform-style\u2019s <code>preserve-3d<\/code> on all divs to position them in the 3D space<\/li>\n\n\n\n<li>Calculate the normalized value of all items by dividing the index by the total number of images<\/li>\n\n\n\n<li>Calculate the brightness of each image container by multiplying the normalized value by 0.9<\/li>\n\n\n\n<li>Set <code>translateZ()<\/code> based on the index of each element multiplied by an offset value. i.e in my case, it is <code>1.5px<\/code> for the first one and <code>0.5px<\/code> for the second, and that\u2019s it!!<\/li>\n<\/ul>\n\n\n\n<p>That was fun! Let me know if you&#8217;ve done this or tried to do something like it in your own work before.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Repeat the same content over and over on top of each other, and you can move each of them just a smidge in 3D space creating the illusion of shape.<\/p>\n","protected":false},"author":38,"featured_media":7824,"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-7802","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\/11\/3dcss.jpg?fit=2000%2C1200&ssl=1","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7802","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\/38"}],"replies":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/comments?post=7802"}],"version-history":[{"count":10,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7802\/revisions"}],"predecessor-version":[{"id":7825,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/posts\/7802\/revisions\/7825"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media\/7824"}],"wp:attachment":[{"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/media?parent=7802"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/categories?post=7802"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/frontendmasters.com\/blog\/wp-json\/wp\/v2\/tags?post=7802"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}