A Material Design surface/card Lit Web Component (<dw-surface>) providing elevation shadows, background color variants, interactive states (hover, focus, ripple), and dark-theme overlay tinting.
Install via npm/yarn:
yarn add @dreamworld/dw-surfacePeer dependencies (must be resolvable in your project):
| Package | Version |
|---|---|
@dreamworld/dw-ripple |
^3.0.0 |
@dreamworld/material-styles |
^3.2.4 |
@dreamworld/pwa-helpers |
^1.13.1 |
Run the demo locally:
yarn start
# Runs: wds --node-resolve --app-index demo/index.html --open --watchThere are two consumption patterns.
Import the component and place content inside the element. Content is rendered via an unnamed <slot>.
<script type="module">
import '@dreamworld/dw-surface';
</script>
<dw-surface elevation="2">
<div>Card content goes here.</div>
</dw-surface>Extend DwSurface and override the _getContentTemplate getter to provide content in Shadow DOM. Call DwSurface.styles in your static styles getter to inherit all base styles.
import { html, css } from 'lit-element';
import { DwSurface } from '@dreamworld/dw-surface';
class MyCard extends DwSurface {
static get styles() {
return [
DwSurface.styles,
css`
:host {
padding: 24px;
width: 288px;
}
`,
];
}
get _getContentTemplate() {
return html`
<h2>My Card Title</h2>
<p>Elevation: ${this.elevation}</p>
${this.interactive ? html`<p>Interactive mode active</p>` : ''}
`;
}
}
customElements.define('my-card', MyCard);<my-card elevation="4" interactive tabindex="0"></my-card>All properties are reflected to their corresponding HTML attributes.
| Name | Type | Default | Description |
|---|---|---|---|
bg |
String |
"surface" |
Background color variant. Accepted values: "surface", "primary", "secondary", "error". |
elevation |
Number |
0 |
Z-depth level. Controls box-shadow via Material elevation tokens. Valid values: 0, 1, 2, 3, 4, 6, 8, 12, 16, 24. |
transparent |
Boolean |
false |
When true and elevation is 0, the background is rendered transparent. Has no effect when elevation > 0. |
interactive |
Boolean |
false |
Enables hover overlay, keyboard focus overlay, and ripple effect on the surface. Set tabindex="0" on the element to make it keyboard-focusable. |
selected |
Boolean |
false |
Renders a persistent selected-state overlay in --mdc-theme-primary color. |
activated |
Boolean |
false |
Renders a persistent activated-state overlay in --mdc-theme-primary color. |
| Attribute | Description |
|---|---|
no-box-shadow |
When present, suppresses the box-shadow that would otherwise be applied by elevation. Handled entirely via CSS attribute selector; not a declared Lit property. |
This component dispatches no custom events. It listens internally to the native mousedown and blur DOM events to manage focus-ring suppression (see Architecture).
| Slot | Description |
|---|---|
| (default) | Content rendered inside the .scroller section. Only used in Pattern 1 (direct use). In Pattern 2 (extension), override _getContentTemplate instead. |
These control the opacity of the white overlay applied over the surface in dark themes to lighten elevated surfaces per Material Design spec. Default values are declared in @dreamworld/material-styles and rarely need overriding.
| Property | Applies at elevation |
|---|---|
--dw-surface-overlay-color |
All elevations (color of the overlay) |
--dw-surface-overlay-opacitiy-elevation-1 |
elevation="1" |
--dw-surface-overlay-opacitiy-elevation-2 |
elevation="2" |
--dw-surface-overlay-opacitiy-elevation-3 |
elevation="3" |
--dw-surface-overlay-opacitiy-elevation-4 |
elevation="4" |
--dw-surface-overlay-opacitiy-elevation-6 |
elevation="6" |
--dw-surface-overlay-opacitiy-elevation-8 |
elevation="8" |
--dw-surface-overlay-opacitiy-elevation-12 |
elevation="12" |
--dw-surface-overlay-opacitiy-elevation-16 |
elevation="16" |
--dw-surface-overlay-opacitiy-elevation-24 |
elevation="24" |
--dw-surface-overlay-border-radius |
Border radius of both overlay divs |
Note: The property names contain a typo (
opacitiy) that is present in the source code. Use the exact spelling above.
These customize the opacity of hover, focus, selected, and activated state overlays. Default values are shown in parentheses. The applicable color token (background-color) is also listed.
| Property | Default | State | bg variant |
|---|---|---|---|
--mdc-theme-on-surface-overlay-opacity-hover |
0.04 |
hover | surface |
--mdc-theme-on-primary-overlay-opacity-hover |
0.08 |
hover | primary |
--mdc-theme-on-secondary-overlay-opacity-hover |
0.08 |
hover | secondary |
--mdc-theme-on-error-overlay-opacity-hover |
0.08 |
hover | error |
--mdc-theme-on-surface-overlay-opacity-focus |
0.12 |
focus | surface |
--mdc-theme-on-primary-overlay-opacity-focus |
0.24 |
focus | primary |
--mdc-theme-on-secondary-overlay-opacity-focus |
0.24 |
focus | secondary |
--mdc-theme-on-error-overlay-opacity-focus |
0.24 |
focus | error |
--mdc-theme-on-surface-overlay-opacity-selected |
0.08 |
selected | surface |
--mdc-theme-on-primary-overlay-opacity-selected |
0.16 |
selected | primary |
--mdc-theme-on-secondary-overlay-opacity-selected |
0.16 |
selected | secondary |
--mdc-theme-on-error-overlay-opacity-selected |
0.16 |
selected | error |
--mdc-theme-on-surface-overlay-opacity-activated |
0.08 |
activated | surface |
--mdc-theme-on-primary-overlay-opacity-activated |
0.24 |
activated | primary |
--mdc-theme-on-secondary-overlay-opacity-activated |
0.24 |
activated | secondary |
--mdc-theme-on-error-overlay-opacity-activated |
0.24 |
activated | error |
--mdc-theme-on-surface-overlay-opacity-activated-selected |
0.12 |
activated + selected | surface |
--mdc-theme-on-surface-overlay-opacity-activated-selected-hover |
0.16 |
activated + selected + hover | surface |
These must be provided by the host application or by @dreamworld/material-styles:
--mdc-theme-surface
--mdc-theme-primary
--mdc-theme-secondary
--mdc-theme-error
--mdc-theme-on-surface
--mdc-theme-on-primary
--mdc-theme-on-secondary
--mdc-theme-on-error
--mdc-elevation--z0 (through --mdc-elevation--z24)
| Property | Description |
|---|---|
--dw-surface-ripple-z-index |
Controls the z-index of the <dw-ripple> element rendered inside the surface when interactive is set. |
When using interactive, add tabindex="0" so the element is reachable by keyboard:
<dw-surface elevation="1" interactive tabindex="0">
<div>Click or tab to me</div>
</dw-surface>The focus ring overlay is suppressed after a mouse click (restored on blur). Keyboard navigation always shows the focus overlay.
transparent only takes effect when elevation="0":
<!-- Background becomes transparent -->
<dw-surface elevation="0" transparent>
<div>Floating content</div>
</dw-surface>
<!-- transparent has NO effect here; bg is still applied -->
<dw-surface elevation="1" transparent>
<div>Still has surface background</div>
</dw-surface>Use the no-box-shadow attribute to retain elevation overlay tinting (dark theme) without the box-shadow:
<dw-surface elevation="4" no-box-shadow>
<div>Elevated tint, no shadow</div>
</dw-surface>Both selected and activated can be set simultaneously. The CSS applies a combined overlay with dedicated opacity tokens:
<dw-surface selected activated>
<div>Selected and activated</div>
</dw-surface>DwSurface is a LitElement custom element following the Web Components v1 standard (Custom Elements + Shadow DOM).
<dw-surface> (shadow root)
├── <dw-ripple disableHover> ← only when [interactive]
├── <div class="overlay fit"> ← dark-theme elevation tinting
├── <section class="scroller">
│ └── <slot> / _getContentTemplate content
└── <div class="overlay1 fit"> ← interactive state overlay (hover/focus/selected/activated)
| Pattern | Where used |
|---|---|
| Template Method | _getContentTemplate getter — base class returns <slot>, subclasses override to provide Shadow DOM content |
| Attribute-reflected properties | All public properties use reflect: true, enabling pure-CSS state machine via attribute selectors |
| CSS State Machine | interactive-Style.js drives all visual state transitions entirely in CSS using :host([attr]) selectors — no JS state logic for hover/focus/selected/activated overlays |
| Separation of Concerns | interactiveStyle is a separate CSS module imported and composed into the component styles array |
Two absolutely-positioned div.fit layers sit over the content:
.overlay— Handles dark-theme elevation tinting. Opacity is set per elevation level via--dw-surface-overlay-opacitiy-elevation-Ncustom properties. Invisible (opacity: 0) in light theme unless customized..overlay1— Handles all interactive state overlays (hover, focus, selected, activated). Driven entirely by CSS rules ininteractive-Style.jsreacting to reflected HTML attributes.
Both layers use pointer-events: none and are not part of the tab order.
Mouse interactions should not show the focus ring; keyboard navigation should. This is implemented without JS state management for the visual effect:
connectedCallbackattachesmousedown→ sets_noFocusEffect = true→ reflects as[no-focus-effect]attribute.disconnectedCallbackremoves both listeners.blurevent → sets_noFocusEffect = false→ removes[no-focus-effect]attribute.- CSS in
interactive-Style.jsexcludes the focus overlay when[no-focus-effect]is present::host(:not([activated]):not([no-focus-effect])[interactive]:focus:not(:focus-within)) .overlay1 { ... }
<dw-ripple disableHover> is conditionally rendered only when interactive=true. The disableHover flag means the ripple component itself does not render hover effects — hover is handled exclusively by the .overlay1 CSS layer. The ripple fires only on pointer/click interaction.
dw-surface.js ← exports class DwSurface, defines <dw-surface>
interactive-Style.js ← exports interactiveStyle (CSS tagged template)