Same element, same script tag, every time. Only the CSS variables change. Open “View code” under any example for the exact markup and custom properties, or check the documentation for the full attribute and CSS variable reference.
Blending, filtering, clipping & masking
DUOTONE
MOTION
A grayscale + contrast filter flattens the video's own color, then hard-light blend-mode lets it react with what's behind it. Diagonal masks on the before and after layers cut solid color blocks behind and in front of the video, at different angles, sandwiching it between two graphic stripes.
View code
<style>
.duotone-motion {
--steam-block-media-aspect-ratio: 16 / 9;
--steam-block-hero-align-items: flex-start;
--steam-block-content-text-align: left;
--steam-block-hero-content-width: 100%;
--steam-block-media-filter: grayscale(1) contrast(1.15);
--steam-block-media-border-radius: 1.25rem;
--steam-block-media-blend-mode: hard-light;
--steam-block-media-before-background: #384f7b;
--steam-block-media-before-mask: linear-gradient(30deg, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 63%,rgba(255,255,255,0) 63%, rgba(255,255,255,0) 75%,rgba(0, 0, 0, 1) 75%, rgba(0, 0, 0, 1) 81%,rgba(255,255,255,0) 81%, rgba(255,255,255,0) 85%,rgba(0, 0, 0,1) 85%, rgba(0, 0, 0,1) 90%,rgba(255,255,255,0) 90%, rgba(255,255,255,0) 100%);
--steam-block-media-after-opacity: .5;
--steam-block-media-after-blend-mode: multiply;
--steam-block-media-after-background: #384f7b;
--steam-block-media-after-mask: linear-gradient(120deg, rgba(0,0,0,1) 0%, rgba(0,0,0,1) 63%,rgba(255,255,255,0) 63%, rgba(255,255,255,0) 75%,rgba(0, 0, 0, 1) 75%, rgba(0, 0, 0, 1) 81%,rgba(255,255,255,0) 81%, rgba(255,255,255,0) 85%,rgba(0, 0, 0,1) 85%, rgba(0, 0, 0,1) 90%,rgba(255,255,255,0) 90%, rgba(255,255,255,0) 100%);
}
/* Below the component's stacking breakpoint (768px default), content
moves under the media instead of overlaying it, so the white text
above needs its own dark backdrop. */
@media (max-width: 47.99rem) {
.example-duotone {
--steam-block-media-border-radius: 1.25rem 1.25rem 0 0;
--steam-block-content-background: #111;
--steam-block-content-padding: 1.5rem;
}
}
</style>
<steam-block class="duotone-motion" type="hero">
<video slot="media" autoplay muted loop>...</video>
<div slot="content"><h1>Duotone motion</h1></div>
</steam-block>COLOR
CLASH
exclusion blend-mode inverts the photo wherever it overlaps the before layer's rounded clip-path cut; a second, larger clip-path shape on the after layer adds a difference blend on top, and the content box gets its own dark overlay tint.
View code
<style>
.color-clash {
--steam-block-background: #0a0a0a;
--steam-block-media-blend-mode: exclusion;
--steam-block-media-before-background: #fffab6;
--steam-block-media-before-clip-path: inset(3vw round 25px);
--steam-block-media-after-blend-mode: difference;
--steam-block-media-after-background: #ffffff;
--steam-block-media-after-clip-path: inset(10vw round 18px);
--steam-block-content-padding: 1rem;
--steam-block-content-border-radius: 1rem;
--steam-block-content-after-blend-mode: overlay;
--steam-block-content-after-background: rgb(0, 0, 0);
}
</style>
<steam-block class="color-clash" type="hero" overlay="true">
<img slot="media" src="photo.jpg" alt="">
<div slot="content"><h1>Color clash</h1></div>
</steam-block>PATTERN
KNOCKOUT
A diagonal mask perforates the background into stripes and a radial mask on the after layer darkens the edges into a vignette. The content box gets its own soft screen-blended glow behind the text, masked to a circle, and a color-burn tint in front.
View code
<style>
.pattern-knockout {
--steam-block-media-aspect-ratio: 2 / 1;
--steam-block-background: #48b4ce;
--steam-block-media-mask: url("diagonal.svg") 0 0/100px 100px repeat;
--steam-block-media-transform: rotate(-35deg) scale(2);
--steam-block-media-after-background: #316099;
--steam-block-media-after-mask: radial-gradient(circle at center center, rgb(0 0 0 / 86%), rgb(0 0 0) 100%);
--steam-block-content-before-background: #e31414;
--steam-block-content-before-blend-mode: screen;
--steam-block-content-before-mask: radial-gradient(circle at center center, rgb(0 0 0 / 82%) 30%, rgb(255 255 255 / 0%) 60%);
--steam-block-content-after-background: #c8b357;
--steam-block-content-after-blend-mode: color-burn;
--steam-block-content-after-mask: url("diagonal.svg") 0 0/175px 175px repeat;
}
/* Let the content layers span the whole hero, not just the text box */
.pattern-knockout::part(content) {
position: static;
}
</style>
<steam-block class="pattern-knockout" type="hero" overlay="true">
<img slot="media" src="photo.jpg" alt="">
<div slot="content"><h1>Pattern knockout</h1></div>
</steam-block>LAYERED
POSTER
Two dot-pattern masks perforate the photo, a fine mesh from the media's own mask, bigger circles from the after layer's, which also carries a black overlay blend, tinting the whole thing a deep purple.
View code
<style>
.layered-poster {
--steam-block-media-aspect-ratio: 5 / 4;
--steam-block-border-radius: 2rem;
--steam-block-hero-align-items: flex-end;
--steam-block-content-text-align: right;
--steam-block-content-transform: rotate(45deg);
--steam-block-background: linear-gradient(135deg, #ffffff, #000000, #dfdbe9);
--steam-block-media-after-background: #57ce51;
--steam-block-media-after-clip-path: polygon(0% 20%, 60% 20%, 60% 0%, 100% 50%, 60% 100%, 60% 80%, 0% 80%);
--steam-block-media-after-blend-mode: overlay;
--steam-block-media-mask: url("dots.svg") 0 0/140px 140px repeat;
}
</style>
<steam-block class="layered-poster" type="hero">
<img slot="media" src="photo.jpg" alt="">
<div slot="content"><h1>Layered poster</h1></div>
</steam-block>Split layouts with borders, shadows & negative white space
PATTERNED
FRAME
The after layer's -media-after-border-image traces a striped SVG tile around the media.
View code
<style>
.patterned-frame {
--steam-block-media-after-border-image: url("crosshatch.svg") 8% / 10px round;
}
</style>
<steam-block class="patterned-frame">
<img slot="media" src="photo.jpg" alt="">
<div slot="content"><h1>Patterned frame</h1></div>
</steam-block>FLOATING
CARD
A hard, offset drop shadow, 1rem 1rem 0 with no blur.
View code
<style>
.floating-card {
--steam-block-max-width: 50rem;
--steam-block-padding: 2rem;
--steam-block-border: 3px solid #1a1a1a;
--steam-block-box-shadow: 1rem 1rem 0 #ffb703;
--steam-block-content-width: 40%;
--steam-block-media-width: 60%;
}
</style>
<steam-block class="floating-card">
<img slot="media" src="photo.jpg" alt="">
<div slot="content"><h1>Floating card</h1></div>
</steam-block>MAGAZINE
OVERLAP
An extreme negative -content-margin, -8rem, pulls the headline hard back over the media edge: an editorial double-page spread in one attribute. media="fluid" brings the image to the edge of the viewport.
View code
<style>
.magazine-overlap {
--steam-block-content-width: 45%;
--steam-block-media-width: 45%;
--steam-block-content-margin: 0 0 0 -50%;
--steam-block-content-padding: 4vw;
--steam-block-content-background: #fff;
}
</style>
<steam-block class="magazine-overlap" order="flip">
<img slot="media" src="photo.jpg" alt="">
<div slot="content"><h1>Magazine overlap</h1></div>
</steam-block>FLOATING
IN SPACE
The JPEG with slot="media" has data-size="auto". This way it gets its own intrinsic size instead of stretching to fill the media area.
With --steam-block-media-margin a negative top margin is set.
View code
<style>
.floating-icon {
--steam-block-background: #eec;
--steam-block-padding: 4rem 3rem;
--steam-block-media-align-items: center;
--steam-block-media-margin: -50% 0 0 0;
}
</style>
<steam-block class="floating-icon" order="flip">
<img data-size="auto" slot="media" src="icon.svg" alt="">
<div slot="content"><h1>Floating in space</h1></div>
</steam-block>ZIG
Alternate order="flip" on the second stea-block and set --steam-block-gap to 0 so all media corners touch.
ZAG
--steam-block-content-padding keeps the text off the media instead.
ZIG
Leave out spacing between the blocks, or the corners drift apart. Works with media="fluid" as well.
View code
<style>
.zigzag {
--steam-block-gap: 0;
--steam-block-content-padding: 0 5vw;
}
</style>
<steam-block class="zigzag">
<img slot="media" src="photo-1.jpg" alt="">
<div slot="content"><h1>Zig</h1></div>
</steam-block>
<steam-block class="zigzag" order="flip">
<img slot="media" src="photo-2.jpg" alt="">
<div slot="content"><h1>Zag</h1></div>
</steam-block>
<steam-block class="zigzag">
<img slot="media" src="photo-3.jpg" alt="">
<div slot="content"><h1>Zig</h1></div>
</steam-block>Real embeds
THE MEDIA
SLOT TAKES
ANY EMBED
A YouTube <iframe>, not an <img>or <video>, following the -media-aspect-ratio which defaults at 16 / 9. Over the video sits a simple -media-after-background gradient. The player's own controls stay fully clickable underneath it.
View code
<style>
.real-embed-hero {
--steam-block-hero-align-items: flex-end;
--steam-block-content-text-align: right;
--steam-block-media-border-radius: 1rem;
--steam-block-media-after-background: linear-gradient(to top, rgba(0, 0, 0, .8) 20%, transparent 65%);
}
</style>
<steam-block class="real-embed-hero" type="hero">
<iframe slot="media" src="https://www.youtube.com/embed/VIDEO_ID" allowfullscreen></iframe>
<div slot="content"><h1>The media slot takes any embed</h1></div>
</steam-block>PORTRAIT VIDEO,
KEPT PORTRAIT
Cropping a vertical video (here from Vimeo) into a 16:9 hero band throws most of it away. type="split" instead gives it its own narrow column at its real aspect ratio, an <iframe>has no natural intrinsic ratio the way <img>/<video> do, so this one sets --steam-block-media-aspect-ratio: 9 / 16;...
View code
<style>
.real-embed-split {
--steam-block-media-aspect-ratio: 9 / 16;
--steam-block-media-width: 38%;
--steam-block-content-width: 62%;
--steam-block-content-padding: 5vw;
--steam-block-content-before-background: white;
--steam-block-content-before-mask: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23FF0066' d='M46.5,-70.2C57.3,-65.5,60.9,-47.5,64.8,-31.7C68.7,-16,72.8,-2.4,69.7,9.1C66.6,20.6,56.3,30.2,46.4,38.3C36.5,46.4,27,53,15.9,58.1C4.8,63.1,-7.9,66.7,-21.9,66.3C-35.9,66,-51.2,61.8,-56.9,51.4C-62.5,41,-58.5,24.4,-61.4,8.4C-64.3,-7.5,-74,-22.9,-70.9,-33.5C-67.8,-44,-51.7,-49.7,-37.7,-52.9C-23.7,-56.2,-11.9,-56.9,3,-61.6C17.9,-66.3,35.8,-74.9,46.5,-70.2Z' transform='translate(100 100) scale(1.35)' /%3E%3C/svg%3E");
--steam-block-content-margin: 0 0 0 -15vw;
--steam-block-media-border-radius: .75rem;
}
</style>
<steam-block class="real-embed-split" order="flip">
<iframe slot="media" src="https://player.vimeo.com/video/VIDEO_ID?background=1" allowfullscreen></iframe>
<div slot="content"><h1>Portrait video, kept portrait</h1></div>
</steam-block>Text columns
ONE COMPONENT
PER LAYOUT
PER LAYOUT
The old way: a hero component, a grid component, a card component, a stats-strip component. Each with its own markup shape, its own CSS, its own quirks to relearn.
ONE COMPONENT,
ANY LAYOUT
The steam-block way: the same element, a few CSS variables. A hero banner this morning, a two-column spread this afternoon, no new markup shape to learn.
View code
<style>
.text-columns {
--steam-block-media-width: 0;
--steam-block-media-min-width: 0;
--steam-block-content-display: grid;
--steam-block-content-grid-template-columns: repeat(auto-fit, minmax(16rem, 1fr));
--steam-block-content-gap: 3rem;
}
</style>
<steam-block class="text-columns">
<div slot="content"><h3>Column one</h3><p>...</p></div>
<div slot="content"><h3>Column two</h3><p>...</p></div>
<!-- no slot="media" element at all -->
</steam-block>View code
<style>
.stat-columns {
--steam-block-media-width: 0;
--steam-block-media-min-width: 0;
--steam-block-content-display: grid;
--steam-block-content-grid-template-columns: repeat(auto-fit, minmax(8rem, 1fr));
--steam-block-content-gap: 1.5rem;
--steam-block-padding: 3rem 2rem;
--steam-block-content-text-align: center;
--steam-block-background: #003049;
--steam-block-border-radius: 1rem;
}
</style>
<steam-block class="stat-columns">
<div slot="content"><strong>~3kb</strong><span>gzipped</span></div>
<div slot="content"><strong>0</strong><span>dependencies</span></div>
<div slot="content"><strong>2</strong><span>slots</span></div>
<div slot="content"><strong>~90</strong><span>CSS variables</span></div>
</steam-block>View code
<style>
.hero-columns {
--steam-block-hero-align-items: stretch;
--steam-block-content-justify-content: flex-end;
--steam-block-content-display: grid;
--steam-block-content-grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
--steam-block-content-gap: 1.5rem;
--steam-block-content-text-align: center;
--steam-block-media-after-background: linear-gradient(to top, rgba(0, 0, 0, .85) 30%, transparent 75%);
}
</style>
<steam-block class="hero-columns" type="hero" overlay="true">
<img slot="media" src="photo.jpg" alt="">
<div slot="content"><strong>Column one</strong></div>
<div slot="content"><strong>Column two</strong></div>
<div slot="content"><strong>Column three</strong></div>
<div slot="content"><strong>Column four</strong></div>
</steam-block>