Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions labs/gb/components/appbar/_app-bar-tokens.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
//
// Copyright 2026 Google LLC
// SPDX-License-Identifier: Apache-2.0
//

// Auto-generated by cascade_tokens
// Add `// EDIT:` comments for manual adjustments.
//
// --prefix=md.comp.app-bar
// --states=":small|[medium-flexible=medium]|[large-flexible=large],with-subtitle,search,on-scroll"
// --exclude=".medium.,.large."

@mixin root {
// LINT.IfChange
// md.comp.app-bar.avatar.size
--avatar-size: 32px;
// md.comp.app-bar.container.color
--container-color: var(--md-sys-color-surface);
// md.comp.app-bar.container.elevation
--container-elevation: var(--md-sys-elevation-shadow-0);
// md.comp.app-bar.small.container.height
--container-height: 64px;
// md.comp.app-bar.container.shape
--container-shape: var(--md-sys-shape-corner-none);
// md.comp.app-bar.icon-button-space
--icon-button-space: var(--md-sys-space-0);
// md.comp.app-bar.icon.size
--icon-size: 24px;
--label-text: unset;
--label-text-axes: unset;
--label-text-tracking: unset;
--label-color: unset;
// md.comp.app-bar.leading-icon.color
--leading-icon-color: var(--md-sys-color-on-surface);
// md.comp.app-bar.leading-space
--leading-space: var(--md-sys-space-50);
// ┌ md.comp.app-bar.small.subtitle.font
--subtitle: var(--md-sys-typescale-label-md);
// ┌┄ md.comp.app-bar.small.subtitle
--subtitle-axes: var(--md-sys-typescale-label-md-axes);
// md.comp.app-bar.subtitle.color
--subtitle-color: var(--md-sys-color-on-surface-variant);
// ┌┄ md.comp.app-bar.small.subtitle
--subtitle-tracking: var(--md-sys-typescale-label-md-tracking);
// ┌ md.comp.app-bar.small.title.font
--title: var(--md-sys-typescale-title-lg);
// ┌┄ md.comp.app-bar.small.title
--title-axes: var(--md-sys-typescale-title-lg-axes);
// md.comp.app-bar.title.color
--title-color: var(--md-sys-color-on-surface);
// ┌┄ md.comp.app-bar.small.title
--title-tracking: var(--md-sys-typescale-title-lg-tracking);
// md.comp.app-bar.trailing-icon.color
--trailing-icon-color: var(--md-sys-color-on-surface-variant);
// md.comp.app-bar.trailing-space
--trailing-space: var(--md-sys-space-50);
// LINT.ThenChange(md-gb-app-bar.ts) keep in sync with `@cssprop` jsdoc tags
}

@mixin medium {
// md.comp.app-bar.medium-flexible.container.height
--container-height: 112px;
// ┌ md.comp.app-bar.medium-flexible.subtitle.font
--subtitle: var(--md-sys-typescale-label-lg);
// ┌┄ md.comp.app-bar.medium-flexible.subtitle
--subtitle-axes: var(--md-sys-typescale-label-lg-axes);
// ┌┄ md.comp.app-bar.medium-flexible.subtitle
--subtitle-tracking: var(--md-sys-typescale-label-lg-tracking);
// ┌ md.comp.app-bar.medium-flexible.title.font
--title: var(--md-sys-typescale-headline-md);
// ┌┄ md.comp.app-bar.medium-flexible.title
--title-axes: var(--md-sys-typescale-headline-md-axes);
// ┌┄ md.comp.app-bar.medium-flexible.title
--title-tracking: var(--md-sys-typescale-headline-md-tracking);
}

@mixin large {
// md.comp.app-bar.large-flexible.container.height
--container-height: 120px;
// ┌ md.comp.app-bar.large-flexible.subtitle.font
--subtitle: var(--md-sys-typescale-title-md);
// ┌┄ md.comp.app-bar.large-flexible.subtitle
--subtitle-axes: var(--md-sys-typescale-title-md-axes);
// ┌┄ md.comp.app-bar.large-flexible.subtitle
--subtitle-tracking: var(--md-sys-typescale-title-md-tracking);
// ┌ md.comp.app-bar.large-flexible.title.font
--title: var(--md-sys-typescale-display-sm);
// ┌┄ md.comp.app-bar.large-flexible.title
--title-axes: var(--md-sys-typescale-display-sm-axes);
// ┌┄ md.comp.app-bar.large-flexible.title
--title-tracking: var(--md-sys-typescale-display-sm-tracking);
}

@mixin with-subtitle {
// ┌ md.comp.app-bar.medium-flexible.with-subtitle.container.height
--container-height: 136px;
}

@mixin with-subtitle-large {
// md.comp.app-bar.large-flexible.with-subtitle.container.height
--container-height: 152px;
}

@mixin search {
// md.comp.app-bar.search.container.color
--container-color: var(--md-sys-color-surface-container);
// md.comp.app-bar.small.search.container.height
--container-height: 56px;
// md.comp.app-bar.small.search.container.shape
--container-shape: var(--md-sys-shape-corner-full);
// ┌ md.comp.app-bar.small.search.label-text.font
--label-text: var(--md-sys-typescale-body-lg);
// ┌┄ md.comp.app-bar.small.search.label-text
--label-text-axes: var(--md-sys-typescale-body-lg-axes);
// ┌┄ md.comp.app-bar.small.search.label-text
--label-text-tracking: var(--md-sys-typescale-body-lg-tracking);
// md.comp.app-bar.search.label.color
--label-color: var(--md-sys-color-on-surface-variant);
// md.comp.app-bar.search.leading-space
--leading-space: var(--md-sys-space-100);
// md.comp.app-bar.search.trailing-space
--trailing-space: var(--md-sys-space-100);
}

@mixin on-scroll {
// md.comp.app-bar.on-scroll.container.color
--container-color: var(--md-sys-color-surface-container);
// md.comp.app-bar.on-scroll.container.elevation
--container-elevation: var(--md-sys-elevation-shadow-2);
}

@mixin on-scroll-search {
// md.comp.app-bar.search.on-scroll.container.color
--container-color: var(--md-sys-color-surface-container-highest);
}

139 changes: 139 additions & 0 deletions labs/gb/components/appbar/app-bar-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {css, CSSResultOrNative, html, LitElement} from 'lit';
import {property, state} from 'lit/decorators.js';

import {appBar, type AppBarSize, type AppBarVariant} from './app-bar.js';
import {styles as appBarStyles} from './app-bar.cssresult.js';

/**
* A Material Design Expressive App Bar component (`md-gb-app-bar`).
*
* @slot - Used to display the primary title text of the app bar.
* @slot subtitle - Used to display an optional subtitle below the title in medium/large bars.
* @slot leading - Used to display the leading navigation icon button (e.g. menu or back).
* @slot trailing - Used to display trailing action buttons and/or user avatar.
* @slot search - Used to display search bar input or controls when `variant="search"`.
* @fires {CustomEvent<{scrolled: boolean}>} scrollstatechange - Fired when the scrolled state changes. --bubbles --composed
* @csspart app-bar - The root container element.
* @cssprop --avatar-size
* @cssprop --container-color
* @cssprop --container-elevation
* @cssprop --container-height
* @cssprop --container-shape
* @cssprop --icon-button-space
* @cssprop --icon-size
* @cssprop --label-text
* @cssprop --label-text-axes
* @cssprop --label-text-tracking
* @cssprop --label-color
* @cssprop --leading-icon-color
* @cssprop --leading-space
* @cssprop --subtitle
* @cssprop --subtitle-axes
* @cssprop --subtitle-color
* @cssprop --subtitle-tracking
* @cssprop --title
* @cssprop --title-axes
* @cssprop --title-color
* @cssprop --title-tracking
* @cssprop --trailing-icon-color
* @cssprop --trailing-space
*/
export class AppBarElement extends LitElement {
static override styles: CSSResultOrNative[] = [
appBarStyles,
css`
:host {
display: flex;
width: 100%;
position: relative;
}
`,
];

/** The size of the app bar (`sm`, `md`, `lg`). Defaults to `sm`. */
@property({reflect: true}) size: AppBarSize = 'sm';

/** The behavioral variant of the app bar (`standard` or `search`). Defaults to `standard`. */
@property({reflect: true}) variant: AppBarVariant = 'standard';

/** Whether the app bar is in its scrolled state. Defaults to `false`. */
@property({type: Boolean, reflect: true}) scrolled = false;

@state() private hasSubtitle = false;

protected override render() {
const withSubtitle = this.size !== 'sm' && this.hasSubtitle;
const directive = appBar({
size: this.size,
withSubtitle,
variant: this.variant,
scrolled: this.scrolled,
});

if (this.variant === 'search') {
return html`<div part="app-bar" class="${directive}">
<div class="app-bar-leading">
<slot name="leading"></slot>
</div>
<div class="app-bar-search-container">
<slot name="search"></slot>
</div>
<div class="app-bar-trailing">
<slot name="trailing"></slot>
</div>
</div>`;
}

if (this.size !== 'sm') {
return html`<div part="app-bar" class="${directive}">
<div class="app-bar-top-row">
<div class="app-bar-leading">
<slot name="leading"></slot>
</div>
<div class="app-bar-trailing">
<slot name="trailing"></slot>
</div>
</div>
<div class="app-bar-bottom-row">
<div class="app-bar-title-wrapper">
<div class="app-bar-title">
<slot></slot>
</div>
<div
class="app-bar-subtitle"
style="${withSubtitle ? '' : 'display: none;'}">
<slot
name="subtitle"
@slotchange=${this.handleSubtitleSlotChange}></slot>
</div>
</div>
</div>
</div>`;
}

return html`<div part="app-bar" class="${directive}">
<div class="app-bar-leading">
<slot name="leading"></slot>
</div>
<div class="app-bar-title-wrapper">
<div class="app-bar-title">
<slot></slot>
</div>
</div>
<div class="app-bar-trailing">
<slot name="trailing"></slot>
</div>
</div>`;
}

private handleSubtitleSlotChange(event: Event) {
const slot = event.target as HTMLSlotElement;
this.hasSubtitle = slot.assignedNodes({flatten: true}).length > 0;
}
}
Loading
Loading