Repository: CrowdStrike/glide-core
Pull request #863: Add Dropdown `loading` attribute
Author: login: clintcs
Created: 2025-04-24T20:09:25Z | Merged: 2025-04-30T14:58:29Z

## 🚀 Description

<!--

  - Tell us about your changes and the motivation for them.
  - Write "N/A" if your changes are explained by the PR's title.

-->


Added a `loading` attribute to Dropdown
## 📋 Checklist

<!-- Do the following before adding reviewers: -->

- I have followed the [Contributing Guidelines](https://github.com/crowdstrike/glide-core/blob/main/CONTRIBUTING.md).
- I have added tests to cover new or updated functionality.
- I have added or updated Storybook stories.
- I have [localized](https://github.com/CrowdStrike/glide-core/blob/main/CONTRIBUTING.md#translations-and-static-strings) new strings.
- I have followed the [ARIA Authoring Practices Guide](https://www.w3.org/WAI/ARIA/apg/patterns/) or met with the Accessibility Team.
- I have included a [changeset](https://github.com/CrowdStrike/glide-core/blob/main/CONTRIBUTING.md#versioning-a-package).
- I have scheduled a design review.
- I have reviewed the Storybook and Visual Test Report links below.

## 🔬 Testing

1. Navigate to Dropdown in Storybook.
2. Set `loading` to `true`.
3. Verify a skeleton loader is shown.
4. Set `loading` to `false`.
5. Verify the skeleton loader is removed.
<!--

  Tell us how to reproduce and verify your changes:

  1. Navigate to Checkbox in Storybook.
  1. Click the label.
  1. Verify Checkbox is checked.

-->



Diff:
diff --git a/.changeset/mighty-crabs-speak.md b/.changeset/mighty-crabs-speak.md
new file mode 100644
index 000000000..2ddb1df9d
--- /dev/null
+++ b/.changeset/mighty-crabs-speak.md
@@ -0,0 +1,5 @@
+---
+'@crowdstrike/glide-core': patch
+---
+
+Dropdown now has a `loading` attribute that shows a skeleton with a fixed number of rows. Add the attribute whenever you asynchronously update Dropdown's default slot. Remove it after the slot has been updated.
diff --git a/.changeset/new-birds-grow.md b/.changeset/new-birds-grow.md
new file mode 100644
index 000000000..dfa6fbfab
--- /dev/null
+++ b/.changeset/new-birds-grow.md
@@ -0,0 +1,5 @@
+---
+'@crowdstrike/glide-core': patch
+---
+
+The spacing around Dropdown's No Results menu has been tightened up.
diff --git a/custom-elements.json b/custom-elements.json
index aa4821fdb..14620f13c 100644
--- a/custom-elements.json
+++ b/custom-elements.json
@@ -2606,6 +2606,16 @@
               "attribute": "hide-label",
               "reflects": true
             },
+            {
+              "kind": "field",
+              "name": "loading",
+              "type": {
+                "text": "boolean"
+              },
+              "default": "false",
+              "attribute": "loading",
+              "reflects": true
+            },
             {
               "kind": "field",
               "name": "name",
@@ -2950,6 +2960,14 @@
               "default": "false",
               "fieldName": "hideLabel"
             },
+            {
+              "name": "loading",
+              "type": {
+                "text": "boolean"
+              },
+              "default": "false",
+              "fieldName": "loading"
+            },
             {
               "name": "name",
               "type": {
diff --git a/src/aria-snapshots/dropdown-test-aria-ts-loading.yml b/src/aria-snapshots/dropdown-test-aria-ts-loading.yml
new file mode 100644
index 000000000..80cb8cc73
--- /dev/null
+++ b/src/aria-snapshots/dropdown-test-aria-ts-loading.yml
@@ -0,0 +1,2 @@
+- text: Label
+- button "Label Loading" [expanded]
\ No newline at end of file
diff --git a/src/dropdown.stories.ts b/src/dropdown.stories.ts
index 11521daff..e31a9797e 100644
--- a/src/dropdown.stories.ts
+++ b/src/dropdown.stories.ts
@@ -43,6 +43,7 @@ const meta: Meta = {
     filterable: false,
     'filter(query)': '',
     'hide-label': false,
+    loading: false,
     multiple: false,
     name: '',
     open: false,
@@ -157,6 +158,18 @@ async (query: string): Promise<GlideCoreDropdownOption[]> => {
         type: { summary: 'boolean' },
       },
     },
+    loading: {
+      table: {
+        defaultValue: { summary: 'false' },
+        type: {
+          summary: 'boolean',
+          detail: `
+// Add this attribute when asynchronously updating Dropdown's default slot. Remove it after the
+// slot has been updated.
+`,
+        },
+      },
+    },
     multiple: {
       table: {
         defaultValue: { summary: 'false' },
@@ -502,6 +515,7 @@ async (query: string): Promise<GlideCoreDropdownOption[]> => {
       variant=${arguments_.variant || nothing}
       ?disabled=${arguments_.disabled}
       ?filterable=${arguments_.filterable}
+      ?loading=${arguments_.loading}
       ?hide-label=${arguments_['hide-label']}
       ?multiple=${arguments_.multiple}
       ?open=${arguments_.open}
@@ -595,6 +609,7 @@ export const WithIcons: StoryObj = {
       variant=${arguments_.variant || nothing}
       ?disabled=${arguments_.disabled}
       ?filterable=${arguments_.filterable}
+      ?loading=${arguments_.loading}
       ?hide-label=${arguments_['hide-label']}
       ?multiple=${arguments_.multiple}
       ?open=${arguments_.open}
diff --git a/src/dropdown.styles.ts b/src/dropdown.styles.ts
index ecad71154..c37c2e808 100644
--- a/src/dropdown.styles.ts
+++ b/src/dropdown.styles.ts
@@ -2,11 +2,13 @@ import { css } from 'lit';
 import focusOutline from './styles/focus-outline.js';
 import opacityAndScaleAnimation from './styles/opacity-and-scale-animation.js';
 import visuallyHidden from './styles/visually-hidden.js';
+import skeleton from './styles/skeleton.js';
 
 export default [
   css`
     ${focusOutline('.add-button:focus-visible')}
     ${opacityAndScaleAnimation('.options-and-footer:popover-open')}
+    ${skeleton('.loading')}
     ${visuallyHidden('.item-count')}
     ${visuallyHidden('.selected-option-labels')}
   `,
@@ -129,9 +131,21 @@ export default [
       border-radius: var(--glide-core-rounding-base-radius-sm);
       box-shadow: var(--glide-core-effect-floating);
       inset: unset;
-      min-inline-size: var(--private-min-inline-size);
       padding: 0;
       position: absolute;
+
+      &.no-results {
+        font-family: var(--glide-core-typography-family-primary);
+        font-size: var(--glide-core-typography-size-body-default);
+        font-weight: var(--glide-core-typography-weight-regular);
+        padding: var(--glide-core-spacing-base-xxs)
+          var(--glide-core-spacing-base-sm);
+        text-transform: capitalize;
+      }
+
+      &:not(.no-results) {
+        min-inline-size: var(--private-min-inline-size);
+      }
     }
 
     .options {
@@ -234,14 +248,6 @@ export default [
       }
     }
 
-    .no-results {
-      font-family: var(--glide-core-typography-family-primary);
-      font-size: var(--glide-core-typography-size-body-default);
-      font-weight: var(--glide-core-typography-weight-regular);
-      padding: 0.625rem 0.875rem;
-      text-transform: capitalize;
-    }
-
     .placeholder {
       color: var(--glide-core-color-interactive-text-placeholder);
 
diff --git a/src/dropdown.test.aria.ts b/src/dropdown.test.aria.ts
index 8fb2cc486..7eb139116 100644
--- a/src/dropdown.test.aria.ts
+++ b/src/dropdown.test.aria.ts
@@ -87,6 +87,21 @@ test('hide-label', async ({ page }, test) => {
   });
 });
 
+test('loading', async ({ page }, test) => {
+  await page.goto('?id=dropdown--dropdown');
+
+  await page
+    .locator('glide-core-dropdown')
+    .evaluate<void, GlideCoreDropdown>((element) => {
+      element.loading = true;
+      element.open = true;
+    });
+
+  await expect(page.locator('glide-core-dropdown')).toMatchAriaSnapshot({
+    name: `${test.titlePath.join('.')}.yml`,
+  });
+});
+
 test.describe('multiple=${true}', () => {
   test('open=${true}', async ({ page }, test) => {
     await page.goto('?id=dropdown--dropdown');
diff --git a/src/dropdown.test.basics.ts b/src/dropdown.test.basics.ts
index e75bd6444..2352bcb9c 100644
--- a/src/dropdown.test.basics.ts
+++ b/src/dropdown.test.basics.ts
@@ -44,6 +44,20 @@ it('can be open', async () => {
   expect(options?.checkVisibility()).to.be.true;
 });
 
+it('can be loading', async () => {
+  const host = await fixture<GlideCoreDropdown>(
+    html`<glide-core-dropdown label="Label" loading open>
+      <glide-core-dropdown-option label="Label"></glide-core-dropdown-option>
+    </glide-core-dropdown>`,
+  );
+
+  // Wait for Floating UI.
+  await aTimeout(0);
+
+  const loading = host.shadowRoot?.querySelector('[data-test="loading"]');
+  expect(loading?.checkVisibility()).to.be.true;
+});
+
 it('selects options when `value` is set initially', async () => {
   const host = await fixture<GlideCoreDropdown>(
     html`<glide-core-dropdown label="Label" .value=${['two']}>
diff --git a/src/dropdown.test.interactions.filterable.ts b/src/dropdown.test.interactions.filterable.ts
index ca9cf33c0..498976afa 100644
--- a/src/dropdown.test.interactions.filterable.ts
+++ b/src/dropdown.test.interactions.filterable.ts
@@ -1550,3 +1550,20 @@ it('retains its filter query when not `multiple` and its default slot changes',
 
   expect(input?.value).to.equal('one');
 });
+
+it('shows a fallback when every option has been filtered out', async () => {
+  const host = await fixture<GlideCoreDropdown>(
+    html`<glide-core-dropdown label="Label" filterable open>
+      <glide-core-dropdown-option label="Label"></glide-core-dropdown-option>
+    </glide-core-dropdown>`,
+  );
+
+  // Wait for Floating UI.
+  await aTimeout(0);
+
+  host.focus();
+  await sendKeys({ type: 'test' });
+
+  const noResults = host.shadowRoot?.querySelector('[data-test="no-results"]');
+  expect(noResults?.checkVisibility()).to.be.true;
+});
diff --git a/src/dropdown.test.visuals.ts b/src/dropdown.test.visuals.ts
index 340c9aee1..fec187ba9 100644
--- a/src/dropdown.test.visuals.ts
+++ b/src/dropdown.test.visuals.ts
@@ -46,7 +46,7 @@ for (const story of stories.Dropdown) {
               element.filterable = true;
             });
 
-          await page.getByRole('combobox').fill('Test');
+          await page.getByRole('combobox').fill('test');
 
           await expect(page).toHaveScreenshot(
             `${test.titlePath.join('.')}.png`,
@@ -103,6 +103,21 @@ for (const story of stories.Dropdown) {
           );
         });
 
+        test('loading', async ({ page }, test) => {
+          await page.goto(`?id=${story.id}&globals=theme:${theme}`);
+
+          await page
+            .locator('glide-core-dropdown')
+            .evaluate<void, GlideCoreDropdown>((element) => {
+              element.loading = true;
+              element.open = true;
+            });
+
+          await expect(page).toHaveScreenshot(
+            `${test.titlePath.join('.')}.png`,
+          );
+        });
+
         test('open', async ({ page }, test) => {
           await page.goto(`?id=${story.id}&globals=theme:${theme}`);
 
diff --git a/src/dropdown.ts b/src/dropdown.ts
index e6a910537..bf112c7dd 100644
--- a/src/dropdown.ts
+++ b/src/dropdown.ts
@@ -9,6 +9,8 @@ import { createRef, ref } from 'lit/directives/ref.js';
 import { customElement, property, state } from 'lit/decorators.js';
 import { ifDefined } from 'lit/directives/if-defined.js';
 import { repeat } from 'lit/directives/repeat.js';
+import { range } from 'lit/directives/range.js';
+import { map } from 'lit/directives/map.js';
 import { styleMap } from 'lit/directives/style-map.js';
 import { unsafeHTML } from 'lit/directives/unsafe-html.js';
 import { when } from 'lit/directives/when.js';
@@ -39,6 +41,7 @@ declare global {
  * @attr {boolean} [disabled=false]
  * @attr {boolean} [filterable=false]
  * @attr {boolean} [hide-label=false]
+ * @attr {boolean} [loading=false]
  * @attr {boolean} [multiple=false]
  * @attr {string} [name='']
  * @attr {boolean} [open=false]
@@ -161,6 +164,9 @@ export default class GlideCoreDropdown
   @property({ attribute: 'hide-label', reflect: true, type: Boolean })
   hideLabel = false;
 
+  @property({ reflect: true, type: Boolean })
+  loading = false;
+
   @property({ reflect: true, useDefault: true })
   name = '';
 
@@ -777,7 +783,7 @@ export default class GlideCoreDropdown
                   aria-controls="options"
                   aria-describedby="description"
                   aria-expanded=${this.open && !this.disabled}
-                  aria-labelledby="selected-option-labels label ${this
+                  aria-labelledby="selected-option-labels label loading ${this
                     .isCommunicateItemCountToScreenreaders
                     ? 'item-count'
                     : ''}"
@@ -918,7 +924,7 @@ export default class GlideCoreDropdown
                 aria-expanded=${this.open && !this.disabled}
                 aria-haspopup="listbox"
                 aria-hidden=${this.filterable || this.isFilterable}
-                aria-labelledby="selected-option-labels label"
+                aria-labelledby="selected-option-labels label loading"
                 class="primary-button"
                 data-test="primary-button"
                 id="primary-button"
@@ -945,19 +951,19 @@ export default class GlideCoreDropdown
           </div>
 
           <div
-            aria-labelledby=${this.filterable || this.isFilterable
-              ? 'input'
-              : 'primary-button'}
-            class="options-and-footer"
+            class=${classMap({
+              'options-and-footer': true,
+              'no-results': this.isNoResults && !this.loading,
+            })}
             ${ref(this.#optionsAndFooterElementRef)}
           >
             <div
               aria-labelledby=${this.filterable || this.isFilterable
                 ? 'input'
-                : 'button'}
+                : 'primary-button'}
               class=${classMap({
                 options: true,
-                hidden: this.isNoResults,
+                hidden: this.isNoResults || this.loading,
                 [this.size]: true,
               })}
               data-test="options"
@@ -1000,8 +1006,18 @@ export default class GlideCoreDropdown
               </slot>
             </div>
 
-            ${when(this.isNoResults, () => {
-              return html`<div class="no-results">
+            ${when(this.loading, () => {
+              return html`<div
+                aria-label=${this.#localize.term('loading')}
+                class="loading"
+                data-test="loading"
+                id="loading"
+              >
+                ${map(range(7), () => html`<div class="bone"></div>`)}
+              </div>`;
+            })}
+            ${when(this.isNoResults && !this.loading, () => {
+              return html`<div data-test="no-results">
                 ${this.#localize.term('noResults')}
               </div>`;
             })}
diff --git a/src/library/localize.ts b/src/library/localize.ts
index 4afcd0cea..16b875373 100644
--- a/src/library/localize.ts
+++ b/src/library/localize.ts
@@ -33,6 +33,7 @@ export interface Translation extends DefaultTranslation {
   success: string;
   error: string;
   informational: string;
+  loading: string;
 
   announcedCharacterCount: (current: number, maximum: number) => string;
   displayedCharacterCount: (current: number, maximum: number) => string;
diff --git a/src/styles/skeleton.ts b/src/styles/skeleton.ts
new file mode 100644
index 000000000..d2b704f33
--- /dev/null
+++ b/src/styles/skeleton.ts
@@ -0,0 +1,49 @@
+import { css, unsafeCSS } from 'lit';
+
+export default (selector: string) => {
+  return css`
+    @keyframes shimmer {
+      0% {
+        background-position: 100% 50%;
+      }
+
+      100% {
+        background-position: 0 50%;
+      }
+    }
+
+    /* stylelint-disable selector-type-case, selector-type-no-unknown */
+    ${unsafeCSS(selector)} {
+      display: flex;
+      flex-direction: column;
+      padding: 0.375rem var(--glide-core-spacing-base-sm);
+      row-gap: var(--glide-core-spacing-base-sm);
+
+      & > * {
+        animation-duration: 1.4s;
+        animation-fill-mode: forwards;
+        animation-iteration-count: infinite;
+        animation-name: shimmer;
+        animation-timing-function: ease;
+        background-image: linear-gradient(
+          90deg,
+          var(
+              --glide-core-private-color-skeleton-loader-surface-linear-gradient-sides
+            )
+            25%,
+          var(
+              --glide-core-private-color-skeleton-loader-surface-linear-gradient-middle
+            )
+            37%,
+          var(
+              --glide-core-private-color-skeleton-loader-surface-linear-gradient-sides
+            )
+            63%
+        );
+        background-size: 400% 100%;
+        block-size: 1rem;
+        border-radius: var(--glide-core-rounding-base-radius-xs);
+      }
+    }
+  `;
+};
diff --git a/src/translations/en.json b/src/translations/en.json
index b2a7eb6e9..9a9c8ffa7 100644
--- a/src/translations/en.json
+++ b/src/translations/en.json
@@ -13,6 +13,7 @@
   "success": "Success:",
   "error": "Error:",
   "informational": "Informational:",
+  "loading": "Loading",
   "announcedCharacterCount": "Character count {current} of {maximum}",
   "displayedCharacterCount": "{current}/{maximum}",
   "clearEntry": "Clear {label} entry",
diff --git a/src/translations/en.ts b/src/translations/en.ts
index 6ccbaac70..b148b47b4 100644
--- a/src/translations/en.ts
+++ b/src/translations/en.ts
@@ -21,6 +21,7 @@ const translation: Translation = {
   success: 'Success:',
   error: 'Error:',
   informational: 'Informational:',
+  loading: 'Loading',
 
   announcedCharacterCount: (current: number, maximum: number) =>
     `Character count ${current} of ${maximum}`,
diff --git a/src/translations/fr.ts b/src/translations/fr.ts
index 32ac5e1da..7ad893dae 100644
--- a/src/translations/fr.ts
+++ b/src/translations/fr.ts
@@ -7,6 +7,7 @@ export const PENDING_STRINGS = [
   'success',
   'error',
   'informational',
+  'loading',
 ] as const;
 
 type PendingTranslation = (typeof PENDING_STRINGS)[number];
diff --git a/src/translations/ja.ts b/src/translations/ja.ts
index faa1a8037..4d9bcfb07 100644
--- a/src/translations/ja.ts
+++ b/src/translations/ja.ts
@@ -7,6 +7,7 @@ export const PENDING_STRINGS = [
   'success',
   'error',
   'informational',
+  'loading',
 ] as const;
 
 type PendingTranslation = (typeof PENDING_STRINGS)[number];


Reviews:
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-25T13:27:13Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T14:14:39Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T15:56:20Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T15:56:31Z
user: login: ynotdraw
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-04-29T16:13:51Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T16:16:25Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-29T18:05:30Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T11:14:08Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T13:06:12Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T13:09:52Z
user: login: ynotdraw
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T14:42:23Z
user: login: clintcs
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-04-30T14:50:55Z

Review comments:
diff_hunk: @@ -945,19 +953,16 @@ export default class GlideCoreDropdown
           </div>
 
           <div
-            aria-labelledby=${this.filterable || this.isFilterable
path: src/dropdown.ts
user: login: clintcs
body: As far as I can tell, there's no benefit to having these here _and_ below. And they have to be on the `role="listbox"` below.
created_at: 2025-04-25T13:27:13Z
author_association: COLLABORATOR
diff_hunk: @@ -2,11 +2,13 @@ import { css } from 'lit';
 import focusOutline from './styles/focus-outline.js';
 import opacityAndScaleAnimation from './styles/opacity-and-scale-animation.js';
 import visuallyHidden from './styles/visually-hidden.js';
+import skeleton from './styles/skeleton.js';
path: src/dropdown.styles.ts
user: login: clintcs
body: A mixin because it'll also be used in Menu, which I'll be working on next.
created_at: 2025-04-29T14:14:39Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { css, unsafeCSS } from 'lit';
+
+export default (container: string, bone: string) => {
path: src/styles/skeleton.ts
user: login: clintcs
body: These make-shift mixins of ours are actually pretty flexible. If we later add another type of skeleton, we can either rename this one to "loading" and have two mixins or else change this one's API:

```ts

export default ({
  selectors: { 
    container: string, 
    bone: string,
  }, 
  type?:  'simple' | 'advanced' = 'simple'
}) => {}
```
created_at: 2025-04-29T15:56:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { css, unsafeCSS } from 'lit';
+
+export default (container: string, bone: string) => {
path: src/styles/skeleton.ts
user: login: clintcs
body: "bone" comes from Design.
created_at: 2025-04-29T15:56:31Z
author_association: COLLABORATOR
diff_hunk: @@ -157,6 +158,18 @@ async (query: string): Promise<GlideCoreDropdownOption[]> => {
         type: { summary: 'boolean' },
       },
     },
+    loading: {
+      table: {
+        defaultValue: { summary: 'false' },
+        type: {
+          summary: 'boolean',
+          detail: `
+// Add this attribute when asychronously updating Dropdown's default slot. Remove it after the
path: src/dropdown.stories.ts
user: login: ynotdraw
body: ```suggestion
// Add this attribute when asynchronously updating Dropdown's default slot. Remove it after the
```


created_at: 2025-04-29T16:10:49Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { css, unsafeCSS } from 'lit';
+
+export default (container: string, bone: string) => {
path: src/styles/skeleton.ts
user: login: ynotdraw
body: 🦴 
created_at: 2025-04-29T16:12:53Z
author_association: COLLABORATOR
diff_hunk: @@ -157,6 +158,18 @@ async (query: string): Promise<GlideCoreDropdownOption[]> => {
         type: { summary: 'boolean' },
       },
     },
+    loading: {
+      table: {
+        defaultValue: { summary: 'false' },
+        type: {
+          summary: 'boolean',
+          detail: `
+// Add this attribute when asychronously updating Dropdown's default slot. Remove it after the
path: src/dropdown.stories.ts
user: login: clintcs
body: Thanks. Trying to meet my typo quota for the day.
created_at: 2025-04-29T16:16:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { css, unsafeCSS } from 'lit';
+
+export default (container: string, bone: string) => {
path: src/styles/skeleton.ts
user: login: clintcs
body: ```suggestion
export default (skeleton: string, bone: string) => {
```

🤔 
created_at: 2025-04-29T18:05:30Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { css, unsafeCSS } from 'lit';
+
+export default (container: string, bone: string) => {
path: src/styles/skeleton.ts
user: login: ynotdraw
body: I kind of like `selector: string` for consistency with the others, despite also liking the idea of going all in on the skeleton/bone naming 😅 
created_at: 2025-04-30T11:14:08Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { css, unsafeCSS } from 'lit';
+
+export default (container: string, bone: string) => {
path: src/styles/skeleton.ts
user: login: clintcs
body: Same. "skeleton" and "bone" are also a little colorful for my taste in this context. But both parameters can't be named "selector".
created_at: 2025-04-30T13:06:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { css, unsafeCSS } from 'lit';
+
+export default (container: string, bone: string) => {
path: src/styles/skeleton.ts
user: login: clintcs
body: Another option would be to have a single parameter and use the direct descendant combinator: 

```ts
export default (selector: string) => {
  ${unsafeCSS(selector)} {
    display: flex;
    flex-direction: column;
    padding: 0.375rem var(--glide-core-spacing-base-sm);
    row-gap: var(--glide-core-spacing-base-sm);
  
    & > * {
      animation-duration: 1.4s;
      animation-fill-mode: forwards;
      animation-iteration-count: infinite;
      animation-name: shimmer;
      animation-timing-function: ease;
      background-image: linear-gradient(
        90deg,
        var(--glide-core-private-color-skeleton-loader-surface-linear-gradient-sides) 25%,
        var(--glide-core-private-color-skeleton-loader-surface-linear-gradient-middle) 37%,
        var(--glide-core-private-color-skeleton-loader-surface-linear-gradient-sides) 63%
      );
      background-size: 400% 100%;
      block-size: 1rem;
      border-radius: var(--glide-core-rounding-base-radius-xs);
    }
  }
}
```
created_at: 2025-04-30T13:09:52Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { css, unsafeCSS } from 'lit';
+
+export default (container: string, bone: string) => {
path: src/styles/skeleton.ts
user: login: ynotdraw
body: I think I like your suggestion above the most 👍 
created_at: 2025-04-30T14:42:23Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,49 @@
+import { css, unsafeCSS } from 'lit';
+
+export default (container: string, bone: string) => {
path: src/styles/skeleton.ts
user: login: clintcs
body: https://github.com/CrowdStrike/glide-core/pull/863/commits/b18cbd994cf48c93ff94f27d7a243f58c05fcdae ✅ 
created_at: 2025-04-30T14:50:55Z
author_association: COLLABORATOR

PR comments:
user: login: changeset-bot[bot]
created_at: 2025-04-24T20:09:29Z
body: ###  🦋  Changeset detected

Latest commit: b18cbd994cf48c93ff94f27d7a243f58c05fcdae

**The changes in this PR will be included in the next version bump.**

<details><summary>This PR includes changesets to release 1 package</summary>

  | Name                    | Type  |
| ----------------------- | ----- |
| @crowdstrike/glide-core | Patch |

</details>

Not sure what this means? [Click here  to learn what changesets are](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add another changeset to this PR](https://github.com/CrowdStrike/glide-core/new/add-dropdown-loading-attribute?filename=.changeset/ten-jeans-shout.md&value=---%0A%22%40crowdstrike%2Fglide-core%22%3A%20patch%0A---%0A%0AAdd%20Dropdown%20%60loading%60%20attribute%0A)


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-04-24T20:11:07Z
body: ## Storybook
https://glide-core.crowdstrike-ux.workers.dev/add-dropdown-loading-attribute
<!-- Sticky Pull Request Commentstorybook -->
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-04-24T20:12:43Z
body: ## ⚠️ Visual Test Report
https://glide-core.crowdstrike-ux.workers.dev/add-dropdown-loading-attribute/visual-test-report
<!-- Sticky Pull Request Commentvisual-test-report -->
author_association: CONTRIBUTOR