diff --git a/all.js b/all.js
index bd5f0a1..c5566dc 100644
--- a/all.js
+++ b/all.js
@@ -4,6 +4,7 @@
* WARNING: This import is intended for prototyping and development builds only.
* Import only the individual components used for production.
*/
+import './app/bar.js'
import './buttons/button.js'
import './buttons/button-group.js'
import './carousel/carousel.js'
@@ -39,6 +40,7 @@ import './text/text-field.js'
// LINT.ThenChange(:exports)
// LINT.IfChange(exports)
// go/keep-sorted start
+export * from './app/bar.js'
export * from './buttons/button.js'
export * from './buttons/button-group.js'
export * from './carousel/carousel.js'
diff --git a/app/README.md b/app/README.md
index d5b8967..b803bd2 100644
--- a/app/README.md
+++ b/app/README.md
@@ -1,6 +1,14 @@
# App Bar
-A Material 3 top app bar component with an integrated search field.
+A Material 3 top app bar component implementing the [Material Design 3 App Bar specification](https://m3.material.io/components/app-bars/overview).
+
+Top app bars display information and actions related to the current screen, supporting:
+
+- **Small App Bar**: Standard 64px height with left-aligned headline.
+- **Center-Aligned App Bar**: 64px height with centered headline.
+- **Medium Flexible App Bar**: Two-row flexible layout (112px–136px) that collapses to 64px on scroll.
+- **Large Flexible App Bar**: Prominent two-row flexible layout (120px–152px) that collapses to 64px on scroll.
+- **Search App Bar**: Top bar with integrated search input field and actions.
## Import
@@ -22,30 +30,105 @@ import 'material/icon/icon.js'
## Usage
+### 1. Small App Bar (Default)
+
+```html
+
+
+ menu
+
+
+
+```
+
+### 3. Medium & Large Flexible App Bars
+
+Flexible app bars support prominent headings and can automatically collapse to 64px when scrolled:
+
```html
-
-
+
+ menu
-
-
- more_vert
-
+
+ filter_list
```
-### Properties and Attributes
+For Large Flexible, use `type="large-flexible"`.
+
+### 4. Search App Bar
+
+```html
+
+
+ menu
+
+
+ mic
+
+
+```
+
+## Properties and Attributes
+
+| Property | Attribute | Type | Default | Description |
+| ---------------- | ----------------- | --------- | ---------- | ----------------------------------------------------------------------------------------------- |
+| `type` | `type` | `string` | `'small'` | Variant: `'small'`, `'center-aligned'`, `'medium-flexible'`, `'large-flexible'`, or `'search'`. |
+| `variant` | `variant` | `string` | `''` | Alias for `type`. |
+| `headline` | `headline` | `string` | `''` | Main headline / title text. |
+| `title` | `title` | `string` | `''` | Alias for `headline`. |
+| `subtitle` | `subtitle` | `string` | `''` | Optional subtitle text. |
+| `centerAligned` | `center-aligned` | `boolean` | `false` | Shorthand boolean to center-align the headline. |
+| `scrolled` | `scrolled` | `boolean` | `false` | Reflects whether container color / elevation is in the scrolled state. |
+| `collapsed` | `collapsed` | `boolean` | `false` | Whether a medium or large flexible bar is collapsed down to 64px. |
+| `scrollBehavior` | `scroll-behavior` | `string` | `'none'` | Auto-scroll mode: `'none'`, `'scroll'` (elevates on scroll), or `'collapse'` (also collapses). |
+| `scrollTarget` | `scroll-target` | `string` | `'window'` | CSS selector of the scroll container element, or `'window'`. |
+| `placeholder` | `placeholder` | `string` | `'Search'` | Placeholder for the search variant input field. |
+| `value` | `value` | `string` | `''` | Current text in the search input field. |
+
+## Slots
-| Property | Attribute | Type | Default | Description |
-| ------------- | ------------- | -------- | ---------- | ----------------------------------------------------- |
-| `label` | `label` | `string` | `'Search'` | The label for the inner search text field. |
-| `placeholder` | `placeholder` | `string` | `'Search'` | The placeholder text for the inner search text field. |
-| `type` | `type` | `string` | `'search'` | Input type for the inner search text field. |
+| Slot | Description |
+| -------------------------- | ------------------------------------------------------------------------------------------ |
+| `navigation-icon` | Leading navigation button (e.g. hamburger menu icon or back arrow). Alias: `leading-icon`. |
+| `headline` | Custom HTML or component for the headline. Alias: `title`. |
+| `subtitle` | Custom HTML or component for the subtitle. |
+| `action-items` | Trailing action icon buttons or user avatar. Alias: `trailing-icon`. |
+| `search-field` | Custom search input field override for the `search` variant. |
+| `leading-icon-text-field` | Icon inside the search input field. |
+| `trailing-icon-text-field` | Clear or trailing icon inside the search input field. |
+| `(default)` | Additional under-bar content (such as tabs or divider). |
-### Slots
+## CSS Custom Properties
-- `leading-icon`: Element placed before the search bar (e.g. navigation drawer menu button).
-- `trailing-icon`: Element placed after the search bar (e.g. profile avatar, action buttons).
-- `leading-icon-text-field`: Leading icon inside the search text field.
-- `trailing-icon-text-field`: Trailing icon inside the search text field.
+| Custom Property | Default Value | Description |
+| --------------------------------------- | ---------------------------------------- | ------------------------------------- |
+| `--md-app-bar-container-color` | `var(--md-sys-color-surface)` | Background color when unscrolled. |
+| `--md-app-bar-scrolled-container-color` | `var(--md-sys-color-surface-container)` | Background color when scrolled. |
+| `--md-app-bar-headline-color` | `var(--md-sys-color-on-surface)` | Text color of the headline. |
+| `--md-app-bar-subtitle-color` | `var(--md-sys-color-on-surface-variant)` | Text color of the subtitle. |
+| `--md-app-bar-leading-icon-color` | `var(--md-sys-color-on-surface)` | Color of the leading navigation icon. |
+| `--md-app-bar-trailing-icon-color` | `var(--md-sys-color-on-surface-variant)` | Color of the trailing action icons. |
diff --git a/app/bar.js b/app/bar.js
index 6a0dd42..521839e 100644
--- a/app/bar.js
+++ b/app/bar.js
@@ -1,59 +1,596 @@
-import { LitElement, html, css } from 'lit'
+/**
+ * @license
+ * Copyright 2024 Google LLC / Material ESM Authors
+ * SPDX-License-Identifier: Apache-2.0
+ */
+import { LitElement, html, css, nothing } from 'lit'
import '../text/text-field.js'
-import { sharedStyles } from '../shared/shared.css.js'
+import '../icon/icon.js'
+import '../internal/elevation/elevation.js'
-class AppBar extends LitElement {
+/**
+ * Material Design 3 Top App Bar component.
+ *
+ * Implements the Material Design 3 App Bar specification:
+ * https://m3.material.io/components/app-bars/overview
+ *
+ * Variants supported:
+ * - `small`: Standard 64px single-row top app bar with left-aligned title.
+ * - `center-aligned`: 64px single-row top app bar with centered title.
+ * - `medium-flexible`: 112px–136px two-row flexible bar, collapses to 64px on scroll.
+ * - `large-flexible`: 120px–152px two-row flexible bar, collapses to 64px on scroll.
+ * - `search`: 64px search bar with embedded search text field.
+ *
+ * @fires search {CustomEvent<{value: string}>} Dispatched when search is submitted.
+ * @fires input {Event} Dispatched when search input value changes.
+ * @fires change {Event} Dispatched when search input commits value.
+ */
+export class AppBar extends LitElement {
static properties = {
+ /** App bar variant: 'small' | 'center-aligned' | 'medium-flexible' | 'large-flexible' | 'search' */
+ type: { type: String, reflect: true },
+ /** Alias for `type` */
+ variant: { type: String },
+ /** Main headline / title text */
+ headline: { type: String },
+ /** Alias for `headline` */
+ title: { type: String },
+ /** Backwards-compatible label property (used as title or search field label) */
label: { type: String },
+ /** Optional subtitle text */
+ subtitle: { type: String },
+ /** Shorthand boolean attribute to center-align the headline */
+ centerAligned: { type: Boolean, attribute: 'center-aligned', reflect: true },
+ /** Whether the app bar has elevation and surface-container color applied from scrolling */
+ scrolled: { type: Boolean, reflect: true },
+ /** Whether a flexible (medium/large) app bar is collapsed down to 64px */
+ collapsed: { type: Boolean, reflect: true },
+ /** Automatic scroll response: 'none' | 'scroll' | 'collapse' */
+ scrollBehavior: { type: String, attribute: 'scroll-behavior' },
+ /** CSS selector of scroll target or 'window' */
+ scrollTarget: { type: String, attribute: 'scroll-target' },
+ /** Placeholder for search variant text field */
placeholder: { type: String },
- type: { type: String },
+ /** Value for search variant text field */
+ value: { type: String },
+ /** Internal state indicating whether search field has text */
+ _hasSearchValue: { state: true },
+ /** Internal state indicating whether custom subtitle markup is slotted */
+ _hasSlottedSubtitle: { state: true },
}
constructor() {
super()
- console.log('SEARCH IS A WIP')
-
- this.label = 'Search'
+ this.type = 'small'
+ this.variant = ''
+ this.headline = ''
+ this.title = ''
+ this.label = ''
+ this.subtitle = ''
+ this.centerAligned = false
+ this.scrolled = false
+ this.collapsed = false
+ this.scrollBehavior = 'none'
+ this.scrollTarget = 'window'
this.placeholder = 'Search'
- this.type = 'search'
+ this.value = ''
+ this._hasSearchValue = false
+ this._hasSlottedSubtitle = false
+
+ this._onScroll = this._handleScroll.bind(this)
+ this._currentTarget = null
+ }
+
+ connectedCallback() {
+ super.connectedCallback()
+ this._attachScrollListener()
+ }
+
+ disconnectedCallback() {
+ super.disconnectedCallback()
+ this._detachScrollListener()
+ }
+
+ updated(changedProperties) {
+ super.updated(changedProperties)
+ if (changedProperties.has('scrollBehavior') || changedProperties.has('scrollTarget')) {
+ this._detachScrollListener()
+ this._attachScrollListener()
+ }
+ if (changedProperties.has('value')) {
+ this._hasSearchValue = Boolean(this.value)
+ }
+ }
+
+ /**
+ * Returns current effective variant type.
+ */
+ get _effectiveType() {
+ const v = (this.variant || this.type || 'small').toLowerCase()
+ if (this.centerAligned || v === 'center-aligned' || v === 'center') {
+ return 'center-aligned'
+ }
+ if (v === 'medium' || v === 'medium-flexible') {
+ return 'medium-flexible'
+ }
+ if (v === 'large' || v === 'large-flexible') {
+ return 'large-flexible'
+ }
+ if (v === 'search') {
+ return 'search'
+ }
+ return 'small'
+ }
+
+ get _isSearch() {
+ return this._effectiveType === 'search'
+ }
+
+ get _isFlexible() {
+ const t = this._effectiveType
+ return t === 'medium-flexible' || t === 'large-flexible'
+ }
+
+ get _isCenterAligned() {
+ return this._effectiveType === 'center-aligned'
+ }
+
+ get _headlineText() {
+ return this.headline || this.title || (!this._isSearch ? this.label : '') || ''
+ }
+
+ get _hasSubtitle() {
+ return Boolean(this.subtitle || this._hasSlottedSubtitle)
+ }
+
+ _handleSubtitleSlotChange(e) {
+ const assigned = e.target.assignedNodes({ flatten: true })
+ this._hasSlottedSubtitle = assigned.some(
+ (n) => n.nodeType === Node.ELEMENT_NODE || (n.textContent || '').trim().length > 0,
+ )
+ }
+
+ _getScrollTargetElement() {
+ if (!this.scrollTarget || this.scrollTarget === 'window') {
+ return window
+ }
+ const root = this.getRootNode()
+ if (root && typeof root.querySelector === 'function') {
+ const el = root.querySelector(this.scrollTarget)
+ if (el) return el
+ }
+ return document.querySelector(this.scrollTarget) || window
+ }
+
+ _attachScrollListener() {
+ if (!this.scrollBehavior || this.scrollBehavior === 'none') {
+ return
+ }
+ const target = this._getScrollTargetElement()
+ if (target) {
+ target.addEventListener('scroll', this._onScroll, { passive: true })
+ this._currentTarget = target
+ // Check initial scroll offset
+ this._handleScroll()
+ }
+ }
+
+ _detachScrollListener() {
+ if (this._currentTarget) {
+ this._currentTarget.removeEventListener('scroll', this._onScroll)
+ this._currentTarget = null
+ }
+ }
+
+ _handleScroll() {
+ const target = this._currentTarget || this._getScrollTargetElement()
+ const scrollTop = target === window ? window.scrollY : target.scrollTop
+ const isScrolled = scrollTop > 4
+
+ if (this.scrolled !== isScrolled) {
+ this.scrolled = isScrolled
+ }
+
+ if (this.scrollBehavior === 'collapse' && this._isFlexible) {
+ const collapseThreshold = this._effectiveType === 'large-flexible' ? 56 : 40
+ const isCollapsed = scrollTop > collapseThreshold
+ if (this.collapsed !== isCollapsed) {
+ this.collapsed = isCollapsed
+ }
+ }
+ }
+
+ _handleSearchInput(e) {
+ const input = e.target
+ this.value = input.value
+ this._hasSearchValue = Boolean(input.value)
+ }
+
+ _handleSearchChange(e) {
+ const input = e.target
+ this.value = input.value
+ }
+
+ _handleSearchKeyDown(e) {
+ if (e.key === 'Enter') {
+ this.dispatchEvent(
+ new CustomEvent('search', {
+ detail: { value: this.value },
+ bubbles: true,
+ composed: true,
+ }),
+ )
+ }
+ }
+
+ _clearSearch() {
+ this.value = ''
+ this._hasSearchValue = false
+ const input = this.renderRoot?.querySelector('#search-input')
+ if (input) {
+ input.value = ''
+ input.dispatchEvent(new Event('input', { bubbles: true, composed: true }))
+ input.dispatchEvent(new Event('change', { bubbles: true, composed: true }))
+ }
}
render() {
+ if (this._isSearch) {
+ return this._renderSearchAppBar()
+ }
+
+ const type = this._effectiveType
+ const isFlexible = this._isFlexible
+ const isCollapsed = isFlexible && this.collapsed
+ const isCenterAligned = this._isCenterAligned
+
return html`
-
+ Scroll the container below to see automatic collapse and elevation changes
+
+
+
+
+
+
+ menu
+
+
+ refresh
+ more_vert
+
+
+
+
+
+ Scroll down to see the medium flexible app bar automatically collapse into a small 64px
+ app bar and raise elevation with the container surface color.
+
+
Item 1: Material Design 3 Expressive typography and colors.
+
Item 2: Seamless transitions between expanded and collapsed states.
+
Item 3: Fully responsive across desktop, tablet, and mobile devices.
+
Item 4: Accessible keyboard navigation and aria landmarks.
+
Item 5: Automatic scroll listeners support either the window or custom container selectors.