—CSS Series · Post 12: Shadows, Depth & Advanced Selectors
Shadows, Depth
& Advanced Selectors
The details that separate "built it" from "designed it." Layered shadows, pseudo-elements that don't need HTML, and precision selectors that target exactly the right element.
💬 "It Looks Flat"
Your layout is solid. Your typography is clean. But the page feels two-dimensional — everything is on the same level, same weight, same presence. Nothing feels like it floats or recedes.
That's a depth problem. Depth in CSS comes from shadows, layering, and intentional use of space. This post covers the tools that create visual hierarchy without changing your layout at all.
🧱 box-shadow
Box shadows add depth below or around an element. The full syntax has five values: horizontal offset, vertical offset, blur radius, spread radius, and color. Most shadows use zero horizontal offset (centered), positive vertical offset (cast downward), and a semi-transparent dark color.
no shadow
subtle
soft
deep
color glow
layered
/* box-shadow: x-offset y-offset blur spread color */
/* Subtle — barely lifts the element */
.card-subtle { box-shadow: 0 2px 8px rgba(0,0,0,0.3); }
/* Soft — standard card shadow */
.card-soft { box-shadow: 0 8px 24px rgba(0,0,0,0.4); }
/* Deep — elevated, prominent */
.card-deep { box-shadow: 0 16px 48px rgba(0,0,0,0.6); }
/* Glow — colored light source */
.card-glow { box-shadow: 0 0 24px rgba(255,105,180,0.25); }
/* Inset — shadow inside the element */
.input-focused { box-shadow: inset 0 0 0 2px #b44aff; }
/* Layered — multiple shadows for realism */
.card-layered {
box-shadow:
0 1px 3px rgba(0,0,0,0.3), /* close, sharp */
0 8px 24px rgba(0,0,0,0.4), /* medium, soft */
0 0 0 1px rgba(255,255,255,0.04); /* inner border */
}Real-world shadows have multiple layers — a close sharp one near the object and a diffuse soft one further away. Combine two or three box-shadows with different blur values and you get depth that feels physical rather than flat. The inner 0 0 0 1px adds a subtle border that doesn't affect layout.
🧱 ::before and ::after — Pseudo-Elements
Pseudo-elements let you insert content before or after an element's content using pure CSS — no extra HTML needed. They're positioned like normal elements and can be styled however you want. Every element gets two free slots: ::before and ::after. The only requirement: they must have a content property (even if it's content: "").
/* Left accent bar — no extra HTML */
.accent-line {
position: relative;
padding-left: 20px;
}
.accent-line::before {
content: ""; /* required — even if empty */
position: absolute;
left: 0;
top: 4px;
width: 3px;
height: calc(100% - 8px);
background: #ff69b4;
border-radius: 2px;
}
/* Decorative quote mark */
.quote::before {
content: '"'; /* actual text content */
font-size: 4rem;
color: rgba(180,74,255,0.3);
position: absolute;
top: -8px;
left: 0;
}
/* Add text after a link */
.external-link::after {
content: ' ↗';
font-size: 0.8em;
opacity: 0.6;
}🧱 Advanced Selectors
Child Selector ( > )
/* .parent p — targets ALL p descendants (nested too) */
.parent p { color: pink; }
/* .parent > p — targets ONLY direct p children */
.parent > p { color: pink; }
/* Useful for nested components that share element names */
.nav > .nav-item { font-weight: 600; } /* direct items only */Attribute Selectors
/* [attr] — has the attribute at all */
input[required] { border-color: #ff69b4; }
/* [attr="value"] — exact match */
input[type="email"] { padding-right: 40px; }
/* [attr^="value"] — starts with */
a[href^="https"] { color: var(--mint-green); } /* secure links */
/* [attr$="value"] — ends with */
a[href$=".pdf"] { } /* PDF links */
/* [attr*="value"] — contains */
a[href*="github"] { } /* any GitHub link */:nth-child() and Structural Pseudo-Classes
/* :first-child / :last-child */
.list-item:first-child { border-top: none; }
.list-item:last-child { border-bottom: none; }
/* :nth-child(n) — every n-th element */
.table-row:nth-child(even) { background: rgba(255,255,255,0.02); }
.table-row:nth-child(odd) { background: transparent; }
.feature:nth-child(3n) { border-color: var(--pink); } /* every 3rd */
/* :not() — exclude specific elements */
.card:not(.card-disabled) { cursor: pointer; }
p:not(.lead) { font-size: 1rem; }
/* :focus-visible — keyboard focus only (accessibility) */
.button:focus-visible {
outline: 2px solid #b44aff;
outline-offset: 3px;
}Mini Build: Cards with Depth + Decorative ::before
Add layered shadows to your card component, and use ::before to create a decorative accent bar — no extra HTML.
Add layered shadows to the card. Three-layer shadow for realistic depth, with a colored glow on hover.
.card {
position: relative; /* needed for ::before */
background: #12121a;
border-radius: 12px;
padding: 28px;
overflow: hidden; /* clips the ::before accent */
box-shadow:
0 1px 3px rgba(0,0,0,0.3),
0 8px 24px rgba(0,0,0,0.4),
0 0 0 1px rgba(255,255,255,0.04);
transition: box-shadow 0.3s ease, transform 0.3s ease;
}
.card:hover {
transform: translateY(-4px);
box-shadow:
0 4px 8px rgba(0,0,0,0.3),
0 16px 40px rgba(0,0,0,0.5),
0 0 0 1px rgba(255,105,180,0.2),
0 0 40px rgba(255,105,180,0.08);
}Add a top accent bar with ::before. A thin color bar across the top of the card — purely decorative, zero HTML.
.card::before {
content: "";
position: absolute;
top: 0;
left: 0;
right: 0;
height: 2px;
background: var(--gradient-header);
opacity: 0;
transition: opacity 0.3s ease;
}
.card:hover::before {
opacity: 1; /* bar appears on hover */
}
/* Now every .card gets this effect with no extra HTML */💛 You're Building Things That Look Designed
Shadows, pseudo-elements, and precise selectors are the finishing layer. They're what makes someone look at your project and think "this looks polished" without being able to articulate exactly why.
- box-shadow takes x, y, blur, spread, and color — stack multiple for realism
- Colored glows use transparent RGBA with no y-offset and high blur
- ::before and ::after insert CSS-only decorative content — always need content: ""
- Pseudo-elements need position: absolute and a position: relative parent to be placed precisely
- Attribute selectors target elements by their HTML attributes — great for forms
- :nth-child() targets structural positions — alternating rows, every 3rd item
- :not() excludes elements from a rule cleanly
Next: Clean CSS Architecture — organizing your stylesheet, naming conventions, BEM basics, and debugging with DevTools. This is the post that makes your CSS maintainable long-term.
I used to add extra <div> elements just for decorative purposes. A thin line above a card, a colored dot before a list item, a quote mark before a blockquote — all had their own HTML elements, sitting in the markup doing nothing structural.
Then I learned ::before and ::after. Every one of those decorative elements disappeared from the HTML. The markup got cleaner, the CSS got more expressive, and I stopped feeling like I was polluting the structure with presentation.
If you have an element whose sole purpose is to look like something, it's probably a pseudo-element waiting to be freed from the HTML.