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.

2 Likes

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!

1 Like

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.

1 Like