Parts

CSS variables are the main way to style steam-block, and they cover most designs. For everything beyond that, every box inside the component is also exposed as a part. With the ::part() selector your own stylesheet can reach that box and use any CSS property on it: hover states, keyframe animations, backdrop-filter, and anything else the variables don’t offer.

Parts are optional. If you never write a ::part() rule, nothing changes.

The concept

steam-block renders its layout in a shadow DOM, so normal selectors like steam-block .media can’t reach inside. A part is a named door into that shadow DOM:

/* CSS variable: the component applies the value for you */
steam-block {
  --steam-block-media-border-radius: 1.5rem;
}

/* Part: you style the media box yourself, any property */
steam-block::part(media) {
  border-radius: 1.5rem;
  outline: 2px solid white;
  outline-offset: -1rem;
}

Available parts

PartWhat it isTemplate
sectionThe outer box of the componentboth
mediaThe box around the media slotboth
contentThe box around the content slotboth
content-innerThe wrapper directly around the slotted contentboth
left / rightThe content column and the media columnsplit
background / foregroundThe media layer and the content overlayhero

The before and after layers are reachable too: ::part(media)::before, ::part(media)::after, ::part(content)::before and ::part(content)::after.

To target one template, combine the part with the host attribute: steam-block[type="hero"]::part(content). split is the default template, so steam-block:not([type="hero"]) targets every split.

Examples

Hover state on the content box

CSS variables can only react to hovering the whole component. A part can react to hovering the box itself. The look comes from the variables, the part only adds the interaction:

<style>
  .parts-lift {
    --steam-block-background: #f1f1f1;
    --steam-block-padding: 2rem;
    --steam-block-content-background: #fff;
    --steam-block-content-padding: 1.5rem;
    --steam-block-content-border-radius: .75rem;
  }
  .parts-lift::part(content) {
    transition: transform .3s, box-shadow .3s;
  }
  .parts-lift::part(content):hover {
    transform: translateY(-.5rem);
    box-shadow: 0 1rem 2rem rgba(0, 0, 0, .2);
  }
</style>
<steam-block class="parts-lift">
  <div slot="content">
    <h3>Hover this card</h3>
    <p>Variables for the look, a part for the interaction.</p>
  </div>
  <img slot="media" src="your-image.jpg" alt="">
</steam-block>

Hover this card

Variables for the look, a part for the interaction.

An animated sheen on the media

The after layer sits on top of the media and is clipped to it. With a part you can animate it with your own keyframes:

<style>
  @keyframes parts-sheen {
    from { transform: translateX(-100%); }
    to   { transform: translateX(100%); }
  }
  .parts-sheen::part(media)::after {
    background: linear-gradient(100deg, transparent 30%, rgba(255, 255, 255, .5) 50%, transparent 70%);
    animation: parts-sheen 3s ease-in-out infinite;
  }
</style>
<steam-block class="parts-sheen" type="hero">
  <img slot="media" src="your-photo.jpg" alt="">
  <div slot="content"><h2>Sheen</h2></div>
</steam-block>

Sheen

A frosted glass overlay in a hero

backdrop-filter has no CSS variable. A part gives you any property, here scoped to heroes only:

<style>
  steam-block[type="hero"].parts-glass::part(content) {
    background: rgba(255, 255, 255, .15);
    backdrop-filter: blur(12px);
    border-radius: 1rem;
  }
  .parts-glass h2,
  .parts-glass p {
    color: #fff;
  }
</style>
<steam-block class="parts-glass" type="hero">
  <img slot="media" src="your-photo.jpg" alt="">
  <div slot="content">
    <h2>Frosted glass</h2>
    <p>The photo blurs behind the text.</p>
  </div>
</steam-block>

Frosted glass

The photo blurs behind the text.

The text color is set on the slotted h2 and p directly. They live in your own page, so plain CSS reaches them without a part.

An animated gradient border

A conic gradient painted in the border area of the content box, turning continuously. @property registers the angle as a real <angle>, which is what lets the browser animate it. Technique from How to animate borders in CSS on Let’s Build UI.

<style>
  @property --parts-angle {
    syntax: '<angle>';
    initial-value: 0deg;
    inherits: false;
  }
  @keyframes parts-spin {
    to { --parts-angle: 360deg; }
  }
  .parts-border {
    --steam-block-content-padding: 1.5rem;
  }
  .parts-border::part(content) {
    border: 3px solid transparent;
    border-radius: .75rem;
    background:
      linear-gradient(white, white) padding-box,
      conic-gradient(from var(--parts-angle), #ff5400, #fde68a, #003049, #ff5400) border-box;
    animation: parts-spin 4s linear infinite;
  }
</style>
<steam-block class="parts-border">
  <div slot="content">
    <h3>Animated border</h3>
    <p>A conic gradient turning in the border area.</p>
  </div>
  <img slot="media" src="your-image.jpg" alt="">
</steam-block>

Animated border

A conic gradient turning in the border area.

Both @property and @keyframes go in your own stylesheet, next to the part rule.

Good to know

  • A part rule wins. If you set both a CSS variable and a part rule for the same property, the part rule wins, whatever the specificity. Use that to override a variable on one instance.

  • Leave layout to the variables. display, position, width, height, margin, order and grid properties on section, left, right, background and foreground fight the component’s own layout and can break it. For column widths, gaps, margins and alignment, use the CSS variables. They also follow the breakpoint for you.

  • Breakpoints are yours. A part rule applies at every width. To match the component’s breakpoint, wrap it in your own media query with the same value as --steam-block-min-width-breakpoint (768px by default):

    @media (min-width: 768px) {
      steam-block::part(content) { backdrop-filter: blur(12px); }
    }
    
  • Stylesheet only. ::part() is a selector, so it can’t go in an inline style="" attribute. CSS variables can.

  • Browser support. ::part() works in every current browser.