CSS box-shadow - a workaround

Hello community,

first, thanks to all people contributing here, I’ve learned a lot in the past weeks.
I want to share a workaround for the missing box-shadow feature.
Since I really would hate to do without it, I’ve written a tiny CSS utility helper, which roughly simulates a drop shadow on an element (actually, it’s just a handful of divs blending into each other).
Maybe it’s useful to someone.

Prerequisites:

  • This works best with small / subtle drop shadows (unless You want to add hundreds of extra divs per element).
  • You don’t mind having a few extra divs per element.
  • Elements with a drop shadow must be positioned and have z-index (s. code comments).
  • Works with border-radius :cyclone::partying_face:.

If You’re fine with that, check out the code comments on how it works and play around with the modifiers, I’ve prepared three of them (–sm, --md, --go-nuts).

CSS-Utitility:

Import this somewhere in Your code.

/* @/css/utilities/box-shadow.css */

/**
 * CSS utility helper to simulate shadows of DOM elements in a
 * Photoshop UXP Plugin.
 * 
 * @author: Cris | 2024 | https://cris.graphics
 *
 * As of August 2024 UXP doesn't support CSS `box-shadow`.
 * This utility aims to simulate the property in a very rudimentary
 * way, by styling a `.box-shadow` container with 5 nested children.
 * You put the `.box-shadow` container as the last child into
 * the main element, You want a drop shadow on.
 * `.box-shadow` and `.box-shadow__bg` elements both inherit the
 * background property of Your main element.
 * The other four children inherit the size of the root element.
 * Each child is larger in size, border-radius and is less opaque
 * than it's previous sibling.
 * So, with 4 'shadow' elements, we obviously can't have a real
 * fading shadow, but for subtle shadows, it's enough to trick
 * the eye.
 *
 * Important note: The `.box-shadow-el` must be positioned
 * (e.g., relative, absolute, fixed) and must have a z-index
 * of at least 0. Otherwise the shadow will disappear behind the
 * container, that `.box-shadow-el` sits in.
 *
 * @usage:
 * <div class="box-shadow-el">
 *   My Element Label
 *   <div class="box-shadow box-shadow--sm">
 *     <div class="box-shadow__bg"></div>
 *     <div class="box-shadow__shadow"></div>
 *     <div class="box-shadow__shadow"></div>
 *     <div class="box-shadow__shadow"></div>
 *     <div class="box-shadow__shadow"></div>
 *   </div>
 * </div>
 */
 

/**
 * Example modifier of how the shadow is applied.
 * Add this modifier to the el containing the `.box-shadow` class.
 *
 * 1. Size in any supported unit.
 * 2. HSL value.
 * 3. Opacity value (0-1).
 * 4. Push the shadow horizontally.
 * 5. Push the shadow vertically.
 * 6. Border-radius of the main element. Maybe set a variable on
 *    Your main element, like `--box-shadow__radius` and
 *    reference it here. Don't reference a variable to itself as
 *    PS might crash.
 * 7. How far to spread the shadow. Offset must be > 0 to take
 *    effect. Can be any number.
 * 8. Blending direction of each shadow layer.
 *    Check out https://developer.mozilla.org/en-US/docs/Web/CSS/gradient/linear-gradient#syntax
 *    for possible values (deg, to top, to bottom, turn, etc.).
 *    If You don't want to blend the shadow layers,
 *    uncomment one of the other `background` props in every
 *    `:nth-child()` selector (s. below).
 */

/* Small size modifier example.  */
.box-shadow--sm {
	--box-shadow__size: 2px; /* 1 */
	--box-shadow__color: 0, 0%, 0%; /* 2 */
	--box-shadow__alpha: .075; /* 3 */
	--box-shadow__offset-x: 1px; /* 4 */
	--box-shadow__offset-y: 2px; /* 5 */
	--box-shadow__rounded: var(--box-shadow__radius, 0px); /* 6 */
	--box-shadow__spread-factor: .25; /* 7 */
	--box-shadow__gradient-direction: to top; /* 8 */
}

/* Medium size modifier example.  */
.box-shadow--md {
	--box-shadow__size: 6px;
	--box-shadow__color: 0, 0%, 0%;
	--box-shadow__alpha: .075;
	--box-shadow__offset-x: 2px;
	--box-shadow__offset-y: 3px;
	--box-shadow__rounded: var(--box-shadow__radius, 0px);
	--box-shadow__spread-factor: .2;
	--box-shadow__gradient-direction: -45deg;
}

/* Whatever-this-is modifier example.  */
.box-shadow--go-nuts {
	--box-shadow__size: .4vw;
	--box-shadow__color: 270, 100%, 0%;
	--box-shadow__alpha: .1;
	--box-shadow__offset-x: .1vw;
	--box-shadow__offset-y: .1vw;
	--box-shadow__rounded: var(--box-shadow__radius, 0px);
	--box-shadow__spread-factor: 15;
	--box-shadow__gradient-direction: 120deg;
}


/**
 * All the nasty code here, which You don't need to worry about,
 * unless You're unhappy with the results ⬇.
 */

/* Container element of the `.box-shadow` item */
.box-shadow-el {
	position: relative;
	z-index: 0;
}

.box-shadow {
	--box-shadow__z: -1;
}

/* Position the shadow and bg color elements. */
.box-shadow,
.box-shadow__bg {
	position: absolute;
	inset: 0;
	width: inherit;
	height: inherit;
	border-radius: inherit;
	background: inherit;
	z-index: var(--box-shadow__z);
}

/* Rules that all shadow layers share. */
.box-shadow > .box-shadow__shadow {
	--__box-shadow__gradient-direction: var(--box-shadow__gradient-direction, 0deg);
	position: inherit;
	top: calc(var(--__box-shadow-size) * -1.5);
	left: calc(var(--__box-shadow-size) * -1.5);
	width: calc(100% + var(--__box-shadow-size));
	height: calc(100%  + var(--__box-shadow-size));
	border-radius: var(--__box-shadow__rounded);
	transform: translate(
		calc(var(--__box-shadow__offset-x) + var(--__box-shadow-size)),
		calc(var(--__box-shadow__offset-y) + var(--__box-shadow-size))
	);
	z-index: calc(var(--box-shadow__z) - 1)
}

/* 1st shadow layer. */
.box-shadow > .box-shadow__shadow:nth-child(2) {
	--__box-shadow-size:  calc(var(--box-shadow__size) * .25);
	--__box-shadow__offset-x: calc(var(--box-shadow__offset-x) * (var(--box-shadow__spread-factor)));
	--__box-shadow__offset-y: calc(var(--box-shadow__offset-y) * (var(--box-shadow__spread-factor)));
	--__box-shadow__rounded: calc(var(--box-shadow__rounded) + (var(--__box-shadow-size) * .25));

	/* background: goldenrod; */
	/* background: hsla(var(--box-shadow__color), calc(var(--box-shadow__alpha) * .75) ); */
	background: linear-gradient(var(--__box-shadow__gradient-direction),
		hsla(var(--box-shadow__color), calc(var(--box-shadow__alpha) * 1)) 0%,
		hsla(var(--box-shadow__color), 0) 100%,
	);
	z-index: calc(var(--box-shadow__z) - 2)
}

/* 2nd shadow layer. */
.box-shadow > .box-shadow__shadow:nth-child(3) {
	--__box-shadow-size: calc(var(--box-shadow__size) * .5);
	--__box-shadow__offset-x: calc(var(--box-shadow__offset-x) * (var(--box-shadow__spread-factor) * 2));
	--__box-shadow__offset-y: calc(var(--box-shadow__offset-y) * (var(--box-shadow__spread-factor) * 2));
	--__box-shadow__rounded: calc(var(--box-shadow__rounded) + (var(--__box-shadow-size) * .5));
	
	/* background: lightseagreen; */
	/* background: hsla(var(--box-shadow__color), calc(var(--box-shadow__alpha) * .5) ); */
	background: linear-gradient(var(--__box-shadow__gradient-direction),
		hsla(var(--box-shadow__color), calc(var(--box-shadow__alpha) * .75)) 0%,
		hsla(var(--box-shadow__color), 0) 100%,
	);
	z-index: calc(var(--box-shadow__z) - 3)
}

/* 3rd shadow layer. */
.box-shadow > .box-shadow__shadow:nth-child(4) {
	--__box-shadow-size: calc(var(--box-shadow__size) * .75);
	--__box-shadow__offset-x: calc(var(--box-shadow__offset-x) * (var(--box-shadow__spread-factor) * 3));
	--__box-shadow__offset-y: calc(var(--box-shadow__offset-y) * (var(--box-shadow__spread-factor) * 3));
	--__box-shadow__rounded: calc(var(--box-shadow__rounded) + (var(--__box-shadow-size) * .75));
	
	/* background: red; */
	/* background: hsla(var(--box-shadow__color), calc(var(--box-shadow__alpha) * .25) ); */
	background: linear-gradient(var(--__box-shadow__gradient-direction),
		hsla(var(--box-shadow__color), calc(var(--box-shadow__alpha) * .5)) 0%,
		hsla(var(--box-shadow__color), 0) 100%,
	);
	z-index: calc(var(--box-shadow__z) - 4)
}

/* 4th shadow layer. */
.box-shadow > .box-shadow__shadow:nth-child(5) {
	--__box-shadow-size: calc(var(--box-shadow__size) * 1);
	--__box-shadow__offset-x: calc(var(--box-shadow__offset-x) * (var(--box-shadow__spread-factor) * 4));
	--__box-shadow__offset-y: calc(var(--box-shadow__offset-y) * (var(--box-shadow__spread-factor) * 4));
	--__box-shadow__rounded: calc(var(--box-shadow__rounded) + (var(--__box-shadow-size) * 1 ));
	
	/* background: blue; */
	/* background: hsla(var(--box-shadow__color), calc(var(--box-shadow__alpha) * .1) ); */
	background: linear-gradient(var(--__box-shadow__gradient-direction),
		hsla(var(--box-shadow__color), calc(var(--box-shadow__alpha) * .25)) 0%,
		hsla(var(--box-shadow__color), 0) 100%,
	);
	z-index: calc(var(--box-shadow__z) - 5)
}

A real world example


<!DOCTYPE html>
<html lang="de">
	<head>
		<meta charset="UTF-8" />
		<meta http-equiv="X-UA-Compatible" content="IE=edge">
		<meta name="viewport" content="width=device-width, initial-scale=1.0" />
		<title>This title is shown nowhere anyway...</title>
		
		<!-- Quick demo. ⬇ -->
		<link rel="stylesheet" href="/src/css/utilities/box-shadow.css" />
		<style>
			.wrapper {
				display: flex;
				flex-direction: column;
				width: 100%;
				height: 300px;
				background: var(--uxp-host-background-color);
				padding: 1rem;
			}
			
			.item {
				--box-shadow__radius: 4px;
				flex: 1;
				display: inline-flex;
				justify-content: center;
				align-items: center;
				padding: .125em .875em;
				color: #333;
				background: #fff;
				border-radius: var(--box-shadow__radius);
			}
			
			.item:nth-child(3) {
				--box-shadow__radius: 10vw;
			}
			
			.item + .item {
				margin-top: 1rem;
			}
		</style>
	</head>
	<body>
		<main>
			<div class="wrapper box-shadow-el">
				<div class="item box-shadow-el">
					Shadow SM
					<div class="box-shadow box-shadow--sm">
						<div class="box-shadow__bg"></div>
						<div class="box-shadow__shadow"></div>
						<div class="box-shadow__shadow"></div>
						<div class="box-shadow__shadow"></div>
						<div class="box-shadow__shadow"></div>
					</div>
				</div>
				<div class="item box-shadow-el">
					Shadow MD
					<div class="box-shadow box-shadow--md">
						<div class="box-shadow__bg"></div>
						<div class="box-shadow__shadow"></div>
						<div class="box-shadow__shadow"></div>
						<div class="box-shadow__shadow"></div>
						<div class="box-shadow__shadow"></div>
					</div>
				</div>
				<div class="item box-shadow-el">
					Go 🥜
					<div class="box-shadow box-shadow--go-nuts">
						<div class="box-shadow__bg"></div>
						<div class="box-shadow__shadow"></div>
						<div class="box-shadow__shadow"></div>
						<div class="box-shadow__shadow"></div>
						<div class="box-shadow__shadow"></div>
					</div>
				</div>
				
				<!-- Put a shadow on the wrapper as well (don't forget
						 to add the `.box-shadow-el` class to it  -->
				<div class="box-shadow box-shadow--md">
					<div class="box-shadow__bg"></div>
					<div class="box-shadow__shadow"></div>
					<div class="box-shadow__shadow"></div>
					<div class="box-shadow__shadow"></div>
					<div class="box-shadow__shadow"></div>
				</div>
			</div>
		</main>
	</body>
</html>

Best,

Cris

PS: Maybe You have already found a different solution. I’m happy to hear, what You’ve come up with.

I think box-shadow should be in UXP v8.0 or?

That would indeed be awesome!
The question is, when will it be released? :slight_smile:

UXP v8.0 It is in PS beta but I am waiting for changelog to see what is there and what not or what flags we need in manifest to turn it on.

Hi Cris,

Your CSS utility for simulating shadows in UXP plugins is quite impressive! I appreciate the thought and detail you’ve put into ensuring it works even with border-radius and the flexibility with different modifiers.

Just a quick note—if you’re looking to keep things a bit simpler or reduce the number of elements in your HTML, you might consider using a standard CSS box-shadow effect where possible. I understand that UXP doesn’t currently support it, but for future-proofing or other environments, it might be worth keeping in mind. Here’s a reference you can check out:
https://www.w3schools.com/css/css3_shadows_box.asp

Thanks for the kind words, Aziz, I appreciate it!
Yes, of course, this is just quite an ugly, temporary solution and should be replaced, as soon as box-shadow is supported in UXP.
However, this shouldn’t be too much of a hassle if one follows the DRY principle.
I also forgot to note, that this preset (as is) won’t work with props like inset or anything alike.
All the best!

I just tried and box-shadow CSS works in UXP 8 in native way. Not sure if SWC flag allows it or not. I am waiting for changelog. But inset does not seem to work yet.

I need this flag in my manifest for box-shadow to work

“featureFlags”: { “enableSWCSupport”: true }