Porky Pig in Pure CSS: A 3D Transforms Deep Dive

That classic Looney Tunes moment—Porky Pig stuttering his way through the closing rings—is a surprisingly apt test case for CSS 3D transforms. The visual trick is that Porky’s lower half disappears behind the bottom edge of the frame while his upper body stacks on top of the top edge. That kind of selective clipping is impossible with standard overflow rules, but it becomes trivial once you start positioning elements in actual 3D space.

Before we get to the cartoon, it helps to understand why this works. In normal CSS flow, a nested element either fits inside its parent or overflows it on all sides equally. There is no way to say "clip the bottom, but let the top stick out." That limitation disappears when you apply 3D transforms. With the right perspective and rotation, an element can physically intersect another one at an angle, which gives you exactly that selective overlap. Tilt a plane slightly toward the viewer, and its top edge will appear in front of the frame while its bottom edge stays behind it.

The Building Blocks of a 3D Scene

To get 3D transforms working correctly, you need two properties:

  • perspective—a pixel value that controls how strong the depth effect is. Lower values produce a more dramatic distortion; higher values flatten the scene.
  • transform-style: preserve-3d—this tells the browser to keep child elements in their 3D positions rather than flattening them into the parent’s plane.

A good starting point for perspective is between 500 and 1,000 pixels. Think of it like vanishing points in a drawing: two close together creates strong perspective, while points far apart make the scene look flat. The same logic applies here.

With that setup, a single <div> plus its two pseudo-elements can generate a convincing three-dimensional intersection. The ::before can act as a frame sitting in one plane, while the ::after is rotated around the X-axis to pass through it. Because the parent has preserve-3d applied, the rotated element is genuinely above the frame at the top edge and behind it at the bottom.

From Rectangles to Porky

Re-creating the animation requires three components layered in 3D space:

  • The <div> itself: the solid blue backdrop.
  • The ::before pseudo-element: Porky, set as a background image.
  • The ::after pseudo-element: the series of red circles that form his entrance tunnel.

The background div also carries the shared properties: perspective, transform-style: preserve-3d, and suitable dimensions to frame the scene.

The trickiest part is the red tunnel. It needs to look like multiple concentric circles, but the center has to remain transparent so Porky can emerge from it. A single border won’t suffice for that pattern. Instead, chained box-shadow values do the job nicely. Each shadow uses a zero blur radius and a large spread radius, effectively creating a series of solid rings.

To build the tunnel:

  • Give the pseudo-element a border-radius equal to half the div’s width so it becomes a circle.
  • Add five spread shadows at 30px intervals to create the distinct rings.
  • Then, on top of those red shadows, add white shadows with a 20px blur to soften the transitions and mimic the glowing effect from the cartoon.

With the backdrop and rings in place, Porky gets added via the background shorthand. Notice the slash syntax in center/contain: the first value sets the position, the second handles the size. Similar to how font-size and line-height are separated in the font shorthand. This same pattern will appear more in future CSS specs—for instance, modern rgb() and hsl() syntax accepts opacity with a slash as in rgb(0 0 0 / 0.5), eliminating the need for the separate rgba() aliases.

Positioning in the Z-Axis

The key step is moving the red circles and Porky to different depths. If you plan to rotate Porky, three things must hold true:

  • He should not clip through the flat blue background.
  • The rotation angle must stay shallow enough to avoid image distortion.
  • His lower half must sit behind the red rings while his torso appears in front of them.

The first problem is solved by nudging the circle pseudo-element toward the viewer along the Z-axis with translateZ(). Even a small shift creates enough separation between the background plane and the rings so that nothing intersects incorrectly.

If you then rotate Porky around his center with rotateX(), his top half angles toward you, but his lower half swings away—straight through the blue backdrop. The common fix is to move him toward the viewer as well, but there is a better one: shift the transform-origin down toward the bottom edge of the image. Now, the whole figure rotates forward instead of pivoting from the middle. Combined with the earlier depth adjustment on the rings, the full effect comes together: Porky’s upper body overlaps the front of the border while his legs vanish behind the tunnel below.

Bringing the Scene to Life

A static image of this isn’t much fun. The whole point is animating the emergence. Porky starts small and low, then scales up to about 60% of his full size, seemingly zooming out from inside the rings.

The animation runs on the ::before pseudo-element using the transform property, which browsers handle at GPU level for smoother performance. It’s important to retain the rotateX(-10deg) value in each animation frame so the 3D intersection stays consistent. That duplication is a temporary annoyance: individual transforms like rotate and scale are on track to become their own CSS properties, which will eventually remove the need to repeat them inside keyframes.

Scaling happens from the bottom-center of Porky’s image because that transform-origin is already set. An ease-in-out timing curve makes the movement feel natural, decelerating at the start and end of the motion. The sequence includes a short pause at maximum scale so Porky lingers fully in view before sinking back down.

Respecting Reduced Motion

For users with the prefers-reduced-motion setting enabled, the full-scale zoom can be replaced with something more subtle. Overriding the @keyframes inside a matching media query lets the browser automatically pick the calmer alternative—a gentle fade rather than the dramatic displacement. Setting animation-iteration-count to 1 in that block shows the effect once and then stops, avoiding sustained or repetitive motion.

Final Polish

Two enhancements elevate the illusion. First, a drop shadow behind Porky that grows as he approaches the camera sells the sense of depth. A standard box-shadow won’t work here because it tracks the element’s rectangular container, not the actual artwork. The filter: drop-shadow() property, however, calculates the shadow from the opaque pixels of the image itself, yielding a silhouette that correctly hugs Porky’s outline.

Second, rotating Porky slightly with rotateZ() during the same animation adds personality, making the pop-out feel less mechanical.

The result is a faithful CSS-only recreation of that outro—no video files, no transparent PNG sequencing, just a single element and a few carefully placed planes in 3D space. And that’s all, folks.