Repository: shoelace-style/shoelace Branch: next Commit: 4fc0fc332ccc Files: 492 Total size: 2.2 MB Directory structure: gitextract_b6qx7r1_/ ├── .editorconfig ├── .eslintignore ├── .eslintrc.cjs ├── .github/ │ ├── CODE_OF_CONDUCT.md │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ └── config.yml │ ├── SECURITY.md │ └── workflows/ │ ├── node.js.yml │ └── release.yml ├── .gitignore ├── .gitpod.yml ├── .husky/ │ └── pre-commit ├── .prettierignore ├── .vscode/ │ ├── extensions.json │ └── settings.json ├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── cspell.json ├── custom-elements-manifest.config.js ├── docs/ │ ├── _includes/ │ │ ├── component.njk │ │ ├── default.njk │ │ ├── sidebar.njk │ │ └── wa-logo-icon.njk │ ├── _utilities/ │ │ ├── active-links.cjs │ │ ├── anchor-headings.cjs │ │ ├── cem.cjs │ │ ├── code-previews.cjs │ │ ├── copy-code-buttons.cjs │ │ ├── external-links.cjs │ │ ├── highlight-code.cjs │ │ ├── markdown.cjs │ │ ├── prettier.cjs │ │ ├── replacer.cjs │ │ ├── scrolling-tables.cjs │ │ ├── strings.cjs │ │ ├── table-of-contents.cjs │ │ └── typography.cjs │ ├── assets/ │ │ ├── examples/ │ │ │ └── include.html │ │ ├── scripts/ │ │ │ ├── code-previews.js │ │ │ ├── docs.js │ │ │ ├── search.js │ │ │ └── turbo.js │ │ └── styles/ │ │ ├── code-previews.css │ │ ├── docs.css │ │ └── search.css │ ├── eleventy.config.cjs │ └── pages/ │ ├── 404.md │ ├── components/ │ │ ├── alert.md │ │ ├── animated-image.md │ │ ├── animation.md │ │ ├── avatar.md │ │ ├── badge.md │ │ ├── breadcrumb-item.md │ │ ├── breadcrumb.md │ │ ├── button-group.md │ │ ├── button.md │ │ ├── card.md │ │ ├── carousel-item.md │ │ ├── carousel.md │ │ ├── checkbox.md │ │ ├── color-picker.md │ │ ├── copy-button.md │ │ ├── details.md │ │ ├── dialog.md │ │ ├── divider.md │ │ ├── drawer.md │ │ ├── dropdown.md │ │ ├── format-bytes.md │ │ ├── format-date.md │ │ ├── format-number.md │ │ ├── icon-button.md │ │ ├── icon.md │ │ ├── image-comparer.md │ │ ├── include.md │ │ ├── input.md │ │ ├── menu-item.md │ │ ├── menu-label.md │ │ ├── menu.md │ │ ├── mutation-observer.md │ │ ├── option.md │ │ ├── popup.md │ │ ├── progress-bar.md │ │ ├── progress-ring.md │ │ ├── qr-code.md │ │ ├── radio-button.md │ │ ├── radio-group.md │ │ ├── radio.md │ │ ├── range.md │ │ ├── rating.md │ │ ├── relative-time.md │ │ ├── resize-observer.md │ │ ├── select.md │ │ ├── skeleton.md │ │ ├── spinner.md │ │ ├── split-panel.md │ │ ├── switch.md │ │ ├── tab-group.md │ │ ├── tab-panel.md │ │ ├── tab.md │ │ ├── tag.md │ │ ├── textarea.md │ │ ├── tooltip.md │ │ ├── tree-item.md │ │ ├── tree.md │ │ └── visually-hidden.md │ ├── frameworks/ │ │ ├── angular.md │ │ ├── react.md │ │ ├── svelte.md │ │ ├── vue-2.md │ │ └── vue.md │ ├── getting-started/ │ │ ├── customizing.md │ │ ├── form-controls.md │ │ ├── installation.md │ │ ├── localization.md │ │ ├── themes.md │ │ └── usage.md │ ├── index.md │ ├── resources/ │ │ ├── accessibility.md │ │ ├── changelog.md │ │ ├── community.md │ │ └── contributing.md │ ├── tokens/ │ │ ├── border-radius.md │ │ ├── color.md │ │ ├── elevation.md │ │ ├── more.md │ │ ├── spacing.md │ │ ├── transition.md │ │ ├── typography.md │ │ └── z-index.md │ └── tutorials/ │ ├── integrating-with-astro.md │ ├── integrating-with-laravel.md │ ├── integrating-with-nextjs.md │ └── integrating-with-rails.md ├── lint-staged.config.js ├── package.json ├── prettier.config.js ├── scripts/ │ ├── build.js │ ├── make-icons.js │ ├── make-metadata.js │ ├── make-react.js │ ├── make-themes.js │ ├── plop/ │ │ ├── plopfile.js │ │ └── templates/ │ │ └── component/ │ │ ├── component.hbs │ │ ├── define.hbs │ │ ├── docs.hbs │ │ ├── styles.hbs │ │ └── tests.hbs │ └── shared.js ├── src/ │ ├── components/ │ │ ├── alert/ │ │ │ ├── alert.component.ts │ │ │ ├── alert.styles.ts │ │ │ ├── alert.test.ts │ │ │ └── alert.ts │ │ ├── animated-image/ │ │ │ ├── animated-image.component.ts │ │ │ ├── animated-image.styles.ts │ │ │ ├── animated-image.test.ts │ │ │ └── animated-image.ts │ │ ├── animation/ │ │ │ ├── animation.component.ts │ │ │ ├── animation.styles.ts │ │ │ ├── animation.test.ts │ │ │ ├── animation.ts │ │ │ └── animations.ts │ │ ├── avatar/ │ │ │ ├── avatar.component.ts │ │ │ ├── avatar.styles.ts │ │ │ ├── avatar.test.ts │ │ │ └── avatar.ts │ │ ├── badge/ │ │ │ ├── badge.component.ts │ │ │ ├── badge.styles.ts │ │ │ ├── badge.test.ts │ │ │ └── badge.ts │ │ ├── breadcrumb/ │ │ │ ├── breadcrumb.component.ts │ │ │ ├── breadcrumb.styles.ts │ │ │ ├── breadcrumb.test.ts │ │ │ └── breadcrumb.ts │ │ ├── breadcrumb-item/ │ │ │ ├── breadcrumb-item.component.ts │ │ │ ├── breadcrumb-item.styles.ts │ │ │ ├── breadcrumb-item.test.ts │ │ │ └── breadcrumb-item.ts │ │ ├── button/ │ │ │ ├── button.component.ts │ │ │ ├── button.styles.ts │ │ │ ├── button.test.ts │ │ │ └── button.ts │ │ ├── button-group/ │ │ │ ├── button-group.component.ts │ │ │ ├── button-group.styles.ts │ │ │ ├── button-group.test.ts │ │ │ └── button-group.ts │ │ ├── card/ │ │ │ ├── card.component.ts │ │ │ ├── card.styles.ts │ │ │ ├── card.test.ts │ │ │ └── card.ts │ │ ├── carousel/ │ │ │ ├── autoplay-controller.ts │ │ │ ├── carousel.component.ts │ │ │ ├── carousel.styles.ts │ │ │ ├── carousel.test.ts │ │ │ └── carousel.ts │ │ ├── carousel-item/ │ │ │ ├── carousel-item.component.ts │ │ │ ├── carousel-item.styles.ts │ │ │ ├── carousel-item.test.ts │ │ │ └── carousel-item.ts │ │ ├── checkbox/ │ │ │ ├── checkbox.component.ts │ │ │ ├── checkbox.styles.ts │ │ │ ├── checkbox.test.ts │ │ │ └── checkbox.ts │ │ ├── color-picker/ │ │ │ ├── color-picker.component.ts │ │ │ ├── color-picker.styles.ts │ │ │ ├── color-picker.test.ts │ │ │ └── color-picker.ts │ │ ├── copy-button/ │ │ │ ├── copy-button.component.ts │ │ │ ├── copy-button.styles.ts │ │ │ ├── copy-button.test.ts │ │ │ └── copy-button.ts │ │ ├── details/ │ │ │ ├── details.component.ts │ │ │ ├── details.styles.ts │ │ │ ├── details.test.ts │ │ │ └── details.ts │ │ ├── dialog/ │ │ │ ├── dialog.component.ts │ │ │ ├── dialog.styles.ts │ │ │ ├── dialog.test.ts │ │ │ └── dialog.ts │ │ ├── divider/ │ │ │ ├── divider.component.ts │ │ │ ├── divider.styles.ts │ │ │ ├── divider.test.ts │ │ │ └── divider.ts │ │ ├── drawer/ │ │ │ ├── drawer.component.ts │ │ │ ├── drawer.styles.ts │ │ │ ├── drawer.test.ts │ │ │ └── drawer.ts │ │ ├── dropdown/ │ │ │ ├── dropdown.component.ts │ │ │ ├── dropdown.styles.ts │ │ │ ├── dropdown.test.ts │ │ │ └── dropdown.ts │ │ ├── format-bytes/ │ │ │ ├── format-bytes.component.ts │ │ │ ├── format-bytes.test.ts │ │ │ └── format-bytes.ts │ │ ├── format-date/ │ │ │ ├── format-date.component.ts │ │ │ ├── format-date.test.ts │ │ │ └── format-date.ts │ │ ├── format-number/ │ │ │ ├── format-number.component.ts │ │ │ ├── format-number.test.ts │ │ │ └── format-number.ts │ │ ├── icon/ │ │ │ ├── icon.component.ts │ │ │ ├── icon.styles.ts │ │ │ ├── icon.test.ts │ │ │ ├── icon.ts │ │ │ ├── library.default.ts │ │ │ ├── library.system.ts │ │ │ └── library.ts │ │ ├── icon-button/ │ │ │ ├── icon-button.component.ts │ │ │ ├── icon-button.styles.ts │ │ │ ├── icon-button.test.ts │ │ │ └── icon-button.ts │ │ ├── image-comparer/ │ │ │ ├── image-comparer.component.ts │ │ │ ├── image-comparer.styles.ts │ │ │ ├── image-comparer.test.ts │ │ │ └── image-comparer.ts │ │ ├── include/ │ │ │ ├── include.component.ts │ │ │ ├── include.styles.ts │ │ │ ├── include.test.ts │ │ │ ├── include.ts │ │ │ └── request.ts │ │ ├── input/ │ │ │ ├── input.component.ts │ │ │ ├── input.styles.ts │ │ │ ├── input.test.ts │ │ │ └── input.ts │ │ ├── menu/ │ │ │ ├── menu.component.ts │ │ │ ├── menu.styles.ts │ │ │ ├── menu.test.ts │ │ │ └── menu.ts │ │ ├── menu-item/ │ │ │ ├── menu-item.component.ts │ │ │ ├── menu-item.styles.ts │ │ │ ├── menu-item.test.ts │ │ │ ├── menu-item.ts │ │ │ └── submenu-controller.ts │ │ ├── menu-label/ │ │ │ ├── menu-label.component.ts │ │ │ ├── menu-label.styles.ts │ │ │ ├── menu-label.test.ts │ │ │ └── menu-label.ts │ │ ├── mutation-observer/ │ │ │ ├── mutation-observer.component.ts │ │ │ ├── mutation-observer.styles.ts │ │ │ ├── mutation-observer.test.ts │ │ │ └── mutation-observer.ts │ │ ├── option/ │ │ │ ├── option.component.ts │ │ │ ├── option.styles.ts │ │ │ ├── option.test.ts │ │ │ └── option.ts │ │ ├── popup/ │ │ │ ├── popup.component.ts │ │ │ ├── popup.styles.ts │ │ │ ├── popup.test.ts │ │ │ └── popup.ts │ │ ├── progress-bar/ │ │ │ ├── progress-bar.component.ts │ │ │ ├── progress-bar.styles.ts │ │ │ ├── progress-bar.test.ts │ │ │ └── progress-bar.ts │ │ ├── progress-ring/ │ │ │ ├── progress-ring.component.ts │ │ │ ├── progress-ring.styles.ts │ │ │ ├── progress-ring.test.ts │ │ │ └── progress-ring.ts │ │ ├── qr-code/ │ │ │ ├── qr-code.component.ts │ │ │ ├── qr-code.styles.ts │ │ │ ├── qr-code.test.ts │ │ │ └── qr-code.ts │ │ ├── radio/ │ │ │ ├── radio.component.ts │ │ │ ├── radio.styles.ts │ │ │ ├── radio.test.ts │ │ │ └── radio.ts │ │ ├── radio-button/ │ │ │ ├── radio-button.component.ts │ │ │ ├── radio-button.styles.ts │ │ │ ├── radio-button.test.ts │ │ │ └── radio-button.ts │ │ ├── radio-group/ │ │ │ ├── radio-group.component.ts │ │ │ ├── radio-group.styles.ts │ │ │ ├── radio-group.test.ts │ │ │ └── radio-group.ts │ │ ├── range/ │ │ │ ├── range.component.ts │ │ │ ├── range.styles.ts │ │ │ ├── range.test.ts │ │ │ └── range.ts │ │ ├── rating/ │ │ │ ├── rating.component.ts │ │ │ ├── rating.styles.ts │ │ │ ├── rating.test.ts │ │ │ └── rating.ts │ │ ├── relative-time/ │ │ │ ├── relative-time.component.ts │ │ │ ├── relative-time.test.ts │ │ │ └── relative-time.ts │ │ ├── resize-observer/ │ │ │ ├── resize-observer.component.ts │ │ │ ├── resize-observer.styles.ts │ │ │ └── resize-observer.ts │ │ ├── select/ │ │ │ ├── select.component.ts │ │ │ ├── select.styles.ts │ │ │ ├── select.test.ts │ │ │ └── select.ts │ │ ├── skeleton/ │ │ │ ├── skeleton.component.ts │ │ │ ├── skeleton.styles.ts │ │ │ ├── skeleton.test.ts │ │ │ └── skeleton.ts │ │ ├── spinner/ │ │ │ ├── spinner.component.ts │ │ │ ├── spinner.styles.ts │ │ │ ├── spinner.test.ts │ │ │ └── spinner.ts │ │ ├── split-panel/ │ │ │ ├── split-panel.component.ts │ │ │ ├── split-panel.styles.ts │ │ │ ├── split-panel.test.ts │ │ │ └── split-panel.ts │ │ ├── switch/ │ │ │ ├── switch.component.ts │ │ │ ├── switch.styles.ts │ │ │ ├── switch.test.ts │ │ │ └── switch.ts │ │ ├── tab/ │ │ │ ├── tab.component.ts │ │ │ ├── tab.styles.ts │ │ │ ├── tab.test.ts │ │ │ └── tab.ts │ │ ├── tab-group/ │ │ │ ├── tab-group.component.ts │ │ │ ├── tab-group.styles.ts │ │ │ ├── tab-group.test.ts │ │ │ └── tab-group.ts │ │ ├── tab-panel/ │ │ │ ├── tab-panel.component.ts │ │ │ ├── tab-panel.styles.ts │ │ │ ├── tab-panel.test.ts │ │ │ └── tab-panel.ts │ │ ├── tag/ │ │ │ ├── tag.component.ts │ │ │ ├── tag.styles.ts │ │ │ ├── tag.test.ts │ │ │ └── tag.ts │ │ ├── textarea/ │ │ │ ├── textarea.component.ts │ │ │ ├── textarea.styles.ts │ │ │ ├── textarea.test.ts │ │ │ └── textarea.ts │ │ ├── tooltip/ │ │ │ ├── tooltip.component.ts │ │ │ ├── tooltip.styles.ts │ │ │ ├── tooltip.test.ts │ │ │ └── tooltip.ts │ │ ├── tree/ │ │ │ ├── tree.component.ts │ │ │ ├── tree.styles.ts │ │ │ ├── tree.test.ts │ │ │ └── tree.ts │ │ ├── tree-item/ │ │ │ ├── tree-item.component.ts │ │ │ ├── tree-item.styles.ts │ │ │ ├── tree-item.test.ts │ │ │ └── tree-item.ts │ │ └── visually-hidden/ │ │ ├── visually-hidden.component.ts │ │ ├── visually-hidden.styles.ts │ │ ├── visually-hidden.test.ts │ │ └── visually-hidden.ts │ ├── declaration.d.ts │ ├── events/ │ │ ├── events.ts │ │ ├── sl-after-collapse.ts │ │ ├── sl-after-expand.ts │ │ ├── sl-after-hide.ts │ │ ├── sl-after-show.ts │ │ ├── sl-blur.ts │ │ ├── sl-cancel.ts │ │ ├── sl-change.ts │ │ ├── sl-clear.ts │ │ ├── sl-close.ts │ │ ├── sl-collapse.ts │ │ ├── sl-copy.ts │ │ ├── sl-error.ts │ │ ├── sl-expand.ts │ │ ├── sl-finish.ts │ │ ├── sl-focus.ts │ │ ├── sl-hide.ts │ │ ├── sl-hover.ts │ │ ├── sl-initial-focus.ts │ │ ├── sl-input.ts │ │ ├── sl-invalid.ts │ │ ├── sl-lazy-change.ts │ │ ├── sl-lazy-load.ts │ │ ├── sl-load.ts │ │ ├── sl-mutation.ts │ │ ├── sl-remove.ts │ │ ├── sl-reposition.ts │ │ ├── sl-request-close.ts │ │ ├── sl-resize.ts │ │ ├── sl-select.ts │ │ ├── sl-selection-change.ts │ │ ├── sl-show.ts │ │ ├── sl-slide-change.ts │ │ ├── sl-start.ts │ │ ├── sl-tab-hide.ts │ │ └── sl-tab-show.ts │ ├── internal/ │ │ ├── active-elements.ts │ │ ├── animate.ts │ │ ├── closeActiveElement.ts │ │ ├── debounce.ts │ │ ├── default-value.ts │ │ ├── drag.ts │ │ ├── event.ts │ │ ├── form.test.ts │ │ ├── form.ts │ │ ├── math.ts │ │ ├── modal.ts │ │ ├── offset.ts │ │ ├── scroll.ts │ │ ├── scrollend-polyfill.ts │ │ ├── shoelace-element.test.ts │ │ ├── shoelace-element.ts │ │ ├── slot.ts │ │ ├── string.ts │ │ ├── tabbable.test.ts │ │ ├── tabbable.ts │ │ ├── test/ │ │ │ ├── data-testid-helpers.ts │ │ │ ├── element-visible-overflow.ts │ │ │ ├── form-control-base-tests.ts │ │ │ └── wait-for-scrolling.ts │ │ ├── test.ts │ │ └── watch.ts │ ├── shoelace-autoloader.ts │ ├── shoelace.ts │ ├── styles/ │ │ ├── component.styles.ts │ │ └── form-control.styles.ts │ ├── themes/ │ │ ├── _utility.css │ │ ├── dark.css │ │ └── light.css │ ├── translations/ │ │ ├── ar.ts │ │ ├── cs.ts │ │ ├── da.ts │ │ ├── de-ch.ts │ │ ├── de.ts │ │ ├── en-gb.ts │ │ ├── en.ts │ │ ├── es.ts │ │ ├── fa.ts │ │ ├── fi.ts │ │ ├── fr.ts │ │ ├── he.ts │ │ ├── hr.ts │ │ ├── hu.ts │ │ ├── id.ts │ │ ├── it.ts │ │ ├── ja.ts │ │ ├── nb.ts │ │ ├── nl.ts │ │ ├── nn.ts │ │ ├── pl.ts │ │ ├── pt.ts │ │ ├── ru.ts │ │ ├── sl.ts │ │ ├── sv.ts │ │ ├── tr.ts │ │ ├── uk.ts │ │ ├── zh-cn.ts │ │ └── zh-tw.ts │ └── utilities/ │ ├── animation-registry.ts │ ├── animation.ts │ ├── base-path.ts │ ├── form.ts │ ├── icon-library.ts │ └── localize.ts ├── tsconfig.json ├── tsconfig.prod.json └── web-test-runner.config.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ # https://editorconfig.org root = true [*] charset = utf-8 indent_style = space indent_size = 2 end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.md] insert_final_newline = false trim_trailing_whitespace = false ================================================ FILE: .eslintignore ================================================ .cache docs/dist docs/search.json docs/**/*.min.js dist examples node_modules src/react scripts ================================================ FILE: .eslintrc.cjs ================================================ /* eslint-env node */ module.exports = { plugins: [ '@typescript-eslint', 'wc', 'lit', 'lit-a11y', 'chai-expect', 'chai-friendly', 'import', 'sort-imports-es6-autofix' ], extends: [ 'eslint:recommended', 'plugin:wc/recommended', 'plugin:wc/best-practice', 'plugin:lit/recommended', 'plugin:lit-a11y/recommended' ], env: { es2021: true, browser: true }, parserOptions: { sourceType: 'module' }, overrides: [ { extends: [ 'plugin:@typescript-eslint/eslint-recommended', 'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended-requiring-type-checking' ], parser: '@typescript-eslint/parser', parserOptions: { sourceType: 'module', project: './tsconfig.json', tsconfigRootDir: __dirname }, files: ['*.ts'], rules: { 'default-param-last': 'off', '@typescript-eslint/default-param-last': 'error', 'no-empty-function': 'off', '@typescript-eslint/no-empty-function': 'warn', 'no-implied-eval': 'off', '@typescript-eslint/no-implied-eval': 'error', 'no-invalid-this': 'off', '@typescript-eslint/no-invalid-this': 'error', 'no-shadow': 'off', '@typescript-eslint/no-shadow': 'error', 'no-throw-literal': 'off', '@typescript-eslint/no-throw-literal': 'error', 'no-unused-expressions': 'off', '@typescript-eslint/prefer-regexp-exec': 'off', '@typescript-eslint/no-unused-expressions': 'error', '@typescript-eslint/unbound-method': 'off', '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-floating-promises': 'off', '@typescript-eslint/no-misused-promises': [ 'error', { checksVoidReturn: false } ], '@typescript-eslint/consistent-type-assertions': [ 'warn', { assertionStyle: 'as', objectLiteralTypeAssertions: 'never' } ], '@typescript-eslint/consistent-type-imports': 'warn', '@typescript-eslint/no-base-to-string': 'error', '@typescript-eslint/no-confusing-non-null-assertion': 'error', '@typescript-eslint/no-invalid-void-type': 'error', '@typescript-eslint/no-require-imports': 'error', '@typescript-eslint/no-unnecessary-boolean-literal-compare': 'warn', '@typescript-eslint/no-unnecessary-condition': 'off', '@typescript-eslint/no-unnecessary-qualifier': 'warn', '@typescript-eslint/no-unnecessary-type-assertion': 'off', '@typescript-eslint/non-nullable-type-assertion-style': 'warn', '@typescript-eslint/prefer-for-of': 'warn', '@typescript-eslint/prefer-optional-chain': 'warn', '@typescript-eslint/prefer-ts-expect-error': 'warn', '@typescript-eslint/prefer-return-this-type': 'error', '@typescript-eslint/prefer-string-starts-ends-with': 'warn', '@typescript-eslint/require-array-sort-compare': 'error', '@typescript-eslint/unified-signatures': 'warn', '@typescript-eslint/array-type': 'warn', '@typescript-eslint/consistent-type-definitions': ['warn', 'interface'], '@typescript-eslint/member-delimiter-style': 'warn', '@typescript-eslint/method-signature-style': 'warn', '@typescript-eslint/no-extraneous-class': 'error', '@typescript-eslint/no-redundant-type-constituents': 'off', '@typescript-eslint/parameter-properties': 'error', '@typescript-eslint/strict-boolean-expressions': 'off' } }, { files: ['**/*.cjs'], env: { node: true } }, { extends: ['plugin:chai-expect/recommended', 'plugin:chai-friendly/recommended'], files: ['*.test.ts'], rules: { '@typescript-eslint/no-unsafe-call': 'off', '@typescript-eslint/no-unused-expressions': 'off' } } ], rules: { 'no-template-curly-in-string': 'error', 'array-callback-return': 'error', 'comma-dangle': 'off', 'consistent-return': 'error', curly: 'off', 'default-param-last': 'error', eqeqeq: 'error', 'lit-a11y/click-events-have-key-events': 'off', 'no-constructor-return': 'error', 'no-empty-function': 'warn', 'no-eval': 'error', 'no-extend-native': 'error', 'no-extra-bind': 'error', 'no-floating-decimal': 'error', 'no-implicit-coercion': 'off', 'no-implicit-globals': 'error', 'no-implied-eval': 'error', 'no-invalid-this': 'error', 'no-labels': 'error', 'no-lone-blocks': 'error', 'no-new': 'error', 'no-new-func': 'error', 'no-new-wrappers': 'error', 'no-octal-escape': 'error', 'no-proto': 'error', 'no-return-assign': 'warn', 'no-script-url': 'error', 'no-self-compare': 'warn', 'no-sequences': 'warn', 'no-throw-literal': 'error', 'no-unmodified-loop-condition': 'error', 'no-unused-expressions': 'warn', 'no-useless-call': 'error', 'no-useless-concat': 'error', 'no-useless-return': 'warn', 'prefer-promise-reject-errors': 'error', radix: 'off', 'require-await': 'error', 'wrap-iife': ['warn', 'inside'], 'no-shadow': 'error', 'no-array-constructor': 'error', 'no-bitwise': 'error', 'no-multi-assign': 'warn', 'no-new-object': 'error', 'no-useless-computed-key': 'warn', 'no-useless-rename': 'warn', 'no-var': 'error', 'prefer-const': 'warn', 'prefer-numeric-literals': 'warn', 'prefer-object-spread': 'warn', 'prefer-rest-params': 'warn', 'prefer-spread': 'warn', 'prefer-template': 'off', 'no-else-return': 'off', 'func-names': ['warn', 'never'], 'one-var': ['warn', 'never'], 'operator-assignment': 'warn', 'prefer-arrow-callback': 'warn', 'no-restricted-imports': [ 'warn', { paths: [ { name: '.', message: 'Usage of local index imports is not allowed.' }, { name: './index', message: 'Import from the source file instead.' } ] } ], 'import/extensions': [ 'error', 'always', { ignorePackages: true, pattern: { js: 'always', ts: 'never' } } ], 'import/no-duplicates': 'warn', 'sort-imports-es6-autofix/sort-imports-es6': [ 2, { ignoreCase: true, ignoreMemberSort: false, memberSyntaxSortOrder: ['none', 'all', 'multiple', 'single'] } ], 'wc/guard-super-call': 'off' } }; ================================================ FILE: .github/CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: - Using welcoming and inclusive language - Being respectful of differing viewpoints and experiences - Gracefully accepting constructive criticism - Focusing on what is best for the community - Showing empathy towards other community members Examples of unacceptable behavior by participants include: - The use of sexualized language or imagery and unwelcome sexual attention or advances - Trolling, insulting/derogatory comments, and personal or political attacks - Public or private harassment - Publishing others' private information, such as a physical or electronic address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies within all project spaces, and it also applies when an individual is representing the project or its community in public spaces. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at cory@abeautifulsite.net. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4. ================================================ FILE: .github/FUNDING.yml ================================================ github: [claviska] ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug Report about: Create a bug report to help us fix a demonstrable problem with code in the library. title: '' labels: bug assignees: --- ### Describe the bug A bug is _a demonstrable problem_ caused by code in the library. Please provide a clear and concise description of what the bug is here. ### To Reproduce Steps to reproduce the behavior: 1. Go to '...' 2. Click on '...' 3. Scroll down to '...' 4. See error ### Demo If the bug isn't obvious, please provide a link to a CodePen or Fiddle with a minimal reproduction. Bugs that have repros get attention faster than those that don't. Tip: use the CodePen button on any example in the docs! ### Screenshots If applicable, add screenshots to help explain the bug. ### Browser / OS - OS: [e.g. Mac, Windows] - Browser: [e.g. Chrome, Firefox, Safari] - Browser version: [e.g. 22] ### Additional information Provide any additional information about the bug here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ contact_links: - name: Feature Requests url: https://github.com/shoelace-style/shoelace/discussions/categories/ideas about: All requests for new features should go here. - name: Help & Support url: https://github.com/shoelace-style/shoelace/discussions/categories/help about: Please don't create issues for personal help requests. Instead, ask your question on the discussion forum. ================================================ FILE: .github/SECURITY.md ================================================ # Reporting Security Issues We take security issues in Shoelace very seriously and appreciate your efforts to disclose your findings responsibly. To report a security issue, email [cory@abeautifulsite.net](mailto:cory@abeautifulsite.net) and include "SHOELACE SECURITY" in the subject line. We'll respond as soon as possible and keep you updated throughout the process. ================================================ FILE: .github/workflows/node.js.yml ================================================ # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions name: Node.js CI on: push: branches: [next] pull_request: branches: [next] jobs: build: runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v3 - name: Update system packages run: | sudo apt-get update sudo apt-get upgrade -y - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v3 with: node-version: ${{ matrix.node-version }} cache: 'npm' - run: npx playwright install --with-deps - run: npm ci - run: npm run verify ================================================ FILE: .github/workflows/release.yml ================================================ # This workflow will create a GitHub release every time a tag is pushed name: Create GitHub Release on: push: tags: - "v2.*" - "v3.*" jobs: release: runs-on: ubuntu-latest steps: - uses: "marvinpinto/action-automatic-releases@v1.2.1" with: repo_token: "${{ secrets.GITHUB_TOKEN }}" prerelease: false ================================================ FILE: .gitignore ================================================ _site .cache .DS_Store cdn dist docs/assets/images/sprite.svg node_modules src/react ================================================ FILE: .gitpod.yml ================================================ tasks: - init: npm install && npm run build command: npm run start ports: - port: 3001 onOpen: ignore - port: 4000-4999 onOpen: open-preview github: prebuilds: # enable for the master/default branch (defaults to true) master: true # enable for all branches in this repo (defaults to false) branches: true # enable for pull requests coming from this repo (defaults to true) pullRequests: true # enable for pull requests coming from forks (defaults to false) pullRequestsFromForks: true # add a check to pull requests (defaults to true) addCheck: true # add a "Review in Gitpod" button as a comment to pull requests (defaults to false) addComment: false # add a "Review in Gitpod" button to the pull request's description (defaults to false) addBadge: true # add a label once the prebuild is ready to pull requests (defaults to false) addLabel: true ================================================ FILE: .husky/pre-commit ================================================ #!/bin/sh . "$(dirname "$0")/_/husky.sh" npx --no-install lint-staged ================================================ FILE: .prettierignore ================================================ *.hbs .cache .github cspell.json dist docs/search.json src/components/icon/icons src/react/index.ts node_modules package.json package-lock.json tsconfig.json cdn _site ================================================ FILE: .vscode/extensions.json ================================================ { "recommendations": [ "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", "bierner.lit-html", "bashmish.es6-string-css", "streetsidesoftware.code-spell-checker" ] } ================================================ FILE: .vscode/settings.json ================================================ { "editor.formatOnSave": true, "editor.defaultFormatter": "esbenp.prettier-vscode", "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" } } ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing to Shoelace Before contributing, please review the contributions guidelines at: [shoelace.style/resources/contributing](https://shoelace.style/resources/contributing) ================================================ FILE: LICENSE.md ================================================ Copyright (c) 2020 A Beautiful Site, LLC Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Shoelace is now Web Awesome 🧡! > [!IMPORTANT] > **Shoelace is in maintenance mode (LTS)**. It is no longer actively being developed but remains available for use under the MIT license. Critical fixes may be released as needed; there is no fixed end date. > For active development and new features, check out Web Awesome at [https://webawesome.com](https://webawesome.com) and [https://github.com/shoelace-style/webawesome](https://github.com/shoelace-style/webawesome). Web Awesome has an even larger library of free web [components](https://webawesome.com/docs/components/), plus [themes](https://webawesome.com/docs/themes/), [utilities](https://webawesome.com/docs/utilities/), [patterns](https://webawesome.com/docs/patterns/), and more. --- # Shoelace A forward-thinking library of web components. - Works with all frameworks 🧩 - Works with CDNs 🚛 - Fully customizable with CSS 🎨 - Includes an official dark theme 🌛 - Built with accessibility in mind ♿️ - Open source 😸 --- - Documentation: [shoelace.style](https://shoelace.style) - Shoelace Source (Maintenance Mode - LTS): [github.com/shoelace-style/shoelace](https://github.com/shoelace-style/shoelace) - Web Awesome Source (Active Development): [github.com/shoelace-style/webawesome](https://github.com/shoelace-style/webawesome) --- ## Shoemakers 🥾 Shoemakers, or "Shoelace developers," can use this documentation to learn how to build Shoelace from source. You will need Node >= 14.17 to build and run the project locally. **You don't need to do any of this to use Shoelace!** This page is for people who want to contribute to the project, tinker with the source, or create a custom build of Shoelace. If that's not what you're trying to do, the [documentation website](https://shoelace.style) is where you want to be. ### What are you using to build Shoelace? Components are built with [LitElement](https://lit-element.polymer-project.org/), a custom elements base class that provides an intuitive API and reactive data binding. The build is a custom script with bundling powered by [esbuild](https://esbuild.github.io/). ### Forking the Repo Start by [forking the repo](https://github.com/shoelace-style/shoelace/fork) on GitHub, then clone it locally and install dependencies. ```bash git clone https://github.com/YOUR_GITHUB_USERNAME/shoelace cd shoelace npm install ``` ### Developing Once you've cloned the repo, run the following command. ```bash npm start ``` This will spin up the dev server. After the initial build, a browser will open automatically. There is currently no hot module reloading (HMR), as browsers don't provide a way to reregister custom elements, but most changes to the source will reload the browser automatically. ### Building To generate a production build, run the following command. ```bash npm run build ``` ### Creating New Components To scaffold a new component, run the following command, replacing `sl-tag-name` with the desired tag name. ```bash npm run create sl-tag-name ``` This will generate a source file, a stylesheet, and a docs page for you. When you start the dev server, you'll find the new component in the "Components" section of the sidebar. ### Contributing Shoelace is open source under the MIT license. Bug fixes and maintenance updates may still be considered; for new features and active development, see [Web Awesome](https://webawesome.com). If you want to contribute here, please review the [contribution guidelines](CONTRIBUTING.md) first. ## License Shoelace is available under the terms of the MIT license. Whether you're building Shoelace or building something _with_ Shoelace — have fun creating! 🥾 ================================================ FILE: cspell.json ================================================ { "version": "0.2", "words": [ "activedescendant", "allowfullscreen", "animationend", "Animista", "apos", "atrule", "autocorrect", "autofix", "autoload", "autoloader", "autoloading", "autoplay", "bezier", "Bokmål", "boxicons", "CACHEABLE", "callout", "callouts", "cdndir", "chatbubble", "checkmark", "claviska", "Clippy", "codebases", "codepen", "colocated", "colour", "combobox", "Commonmark", "Composability", "Consolas", "contenteditable", "copydir", "Cotte", "coverpage", "crossorigin", "crutchcorn", "csspart", "cssproperty", "datetime", "describedby", "Docsify", "dogfood", "dropdowns", "easings", "endraw", "enterkeyhint", "eqeqeq", "erroneou", "errormessage", "esbuild", "exportmaps", "exportparts", "fieldsets", "formaction", "formdata", "formenctype", "formmethod", "formnovalidate", "formtarget", "FOUC", "FOUCE", "fullscreen", "gestern", "giga", "globby", "Grayscale", "haspopup", "heroicons", "hexa", "Iconoir", "Iframes", "iife", "inputmode", "ionicon", "ionicons", "jsDelivr", "jsfiddle", "keydown", "keyframes", "Kool", "labelledby", "Laravel", "LaViska", "linkify", "listbox", "listitem", "litelement", "lowercasing", "Lucide", "maxlength", "Menlo", "menuitemcheckbox", "menuitemradio", "middlewares", "minlength", "monospace", "mousedown", "mousemove", "mouseout", "mouseup", "multiselectable", "nextjs", "nocheck", "noopener", "noreferrer", "novalidate", "npmdir", "Numberish", "onscrollend", "outdir", "ParamagicDev", "peta", "petabit", "prismjs", "progressbar", "radiogroup", "Railsbyte", "remixicon", "reregister", "resizer", "resizers", "retargeted", "RETRYABLE", "rgba", "roadmap", "Roboto", "roledescription", "Sapan", "saturationl", "Schilp", "scrollbars", "scrollend", "scroller", "Segoe", "semibold", "sitedir", "slotchange", "smartquotes", "spacebar", "stylesheet", "Tabbable", "tabindex", "tabler", "tablist", "tabpanel", "templating", "tera", "testid", "textareas", "textfield", "tinycolor", "transitionend", "treeitem", "treeshaking", "Triaging", "turbolinks", "typeof", "unbundles", "unbundling", "unicons", "unsanitized", "unsupportive", "valpha", "valuenow", "valuetext", "vuejs", "WEBP", "Webpacker", "wordmark" ], "ignorePaths": [ "package.json", "package-lock.json", "docs/assets/examples/include.html", ".vscode/**", "src/translations/!(en).ts", "**/*.min.js" ], "ignoreRegExpList": [ "(^|[^a-z])sl[a-z]*(^|[^a-z])" ], "useGitignore": true } ================================================ FILE: custom-elements-manifest.config.js ================================================ import * as path from 'path'; import { customElementJetBrainsPlugin } from 'custom-element-jet-brains-integration'; import { customElementVsCodePlugin } from 'custom-element-vs-code-integration'; import { customElementVuejsPlugin } from 'custom-element-vuejs-integration'; import { parse } from 'comment-parser'; import { pascalCase } from 'pascal-case'; import commandLineArgs from 'command-line-args'; import fs from 'fs'; const packageData = JSON.parse(fs.readFileSync('./package.json', 'utf8')); const { name, description, version, author, homepage, license } = packageData; const { outdir } = commandLineArgs([ { name: 'litelement', type: String }, { name: 'analyze', defaultOption: true }, { name: 'outdir', type: String } ]); function noDash(string) { return string.replace(/^\s?-/, '').trim(); } function replace(string, terms) { terms.forEach(({ from, to }) => { string = string?.replace(from, to); }); return string; } export default { globs: ['src/components/**/*.component.ts'], exclude: ['**/*.styles.ts', '**/*.test.ts'], plugins: [ // Append package data { name: 'shoelace-package-data', packageLinkPhase({ customElementsManifest }) { customElementsManifest.package = { name, description, version, author, homepage, license }; } }, // Infer tag names because we no longer use @customElement decorators. { name: 'shoelace-infer-tag-names', analyzePhase({ ts, node, moduleDoc }) { switch (node.kind) { case ts.SyntaxKind.ClassDeclaration: { const className = node.name.getText(); const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className); const importPath = moduleDoc.path; // This is kind of a best guess at components. "thing.component.ts" if (!importPath.endsWith('.component.ts')) { return; } const tagNameWithoutPrefix = path.basename(importPath, '.component.ts'); const tagName = 'sl-' + tagNameWithoutPrefix; classDoc.tagNameWithoutPrefix = tagNameWithoutPrefix; classDoc.tagName = tagName; // This used to be set to true by @customElement classDoc.customElement = true; } } } }, // Parse custom jsDoc tags { name: 'shoelace-custom-tags', analyzePhase({ ts, node, moduleDoc }) { switch (node.kind) { case ts.SyntaxKind.ClassDeclaration: { const className = node.name.getText(); const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className); const customTags = ['animation', 'dependency', 'documentation', 'since', 'status', 'title']; let customComments = '/**'; node.jsDoc?.forEach(jsDoc => { jsDoc?.tags?.forEach(tag => { const tagName = tag.tagName.getText(); if (customTags.includes(tagName)) { customComments += `\n * @${tagName} ${tag.comment}`; } }); }); // This is what allows us to map JSDOC comments to ReactWrappers. classDoc['jsDoc'] = node.jsDoc?.map(jsDoc => jsDoc.getFullText()).join('\n'); const parsed = parse(`${customComments}\n */`); parsed[0].tags?.forEach(t => { switch (t.tag) { // Animations case 'animation': if (!Array.isArray(classDoc['animations'])) { classDoc['animations'] = []; } classDoc['animations'].push({ name: t.name, description: noDash(t.description) }); break; // Dependencies case 'dependency': if (!Array.isArray(classDoc['dependencies'])) { classDoc['dependencies'] = []; } classDoc['dependencies'].push(t.name); break; // Value-only metadata tags case 'documentation': case 'since': case 'status': case 'title': classDoc[t.tag] = t.name; break; // All other tags default: if (!Array.isArray(classDoc[t.tag])) { classDoc[t.tag] = []; } classDoc[t.tag].push({ name: t.name, description: t.description, type: t.type || undefined }); } }); } } } }, { name: 'shoelace-react-event-names', analyzePhase({ ts, node, moduleDoc }) { switch (node.kind) { case ts.SyntaxKind.ClassDeclaration: { const className = node.name.getText(); const classDoc = moduleDoc?.declarations?.find(declaration => declaration.name === className); if (classDoc?.events) { classDoc.events.forEach(event => { event.reactName = `on${pascalCase(event.name)}`; event.eventName = `${pascalCase(event.name)}Event`; }); } } } } }, { name: 'shoelace-translate-module-paths', packageLinkPhase({ customElementsManifest }) { customElementsManifest?.modules?.forEach(mod => { // // CEM paths look like this: // // src/components/button/button.ts // // But we want them to look like this: // // components/button/button.js // const terms = [ { from: /^src\//, to: '' }, // Strip the src/ prefix { from: /\.component.(t|j)sx?$/, to: '.js' } // Convert .ts to .js ]; mod.path = replace(mod.path, terms); for (const ex of mod.exports ?? []) { ex.declaration.module = replace(ex.declaration.module, terms); } for (const dec of mod.declarations ?? []) { if (dec.kind === 'class') { for (const member of dec.members ?? []) { if (member.inheritedFrom) { member.inheritedFrom.module = replace(member.inheritedFrom.module, terms); } } } } }); } }, // Generate custom VS Code data customElementVsCodePlugin({ outdir, cssFileName: null, referencesTemplate: (_, tag) => [ { name: 'Documentation', url: `https://shoelace.style/components/${tag.replace('sl-', '')}` } ] }), customElementJetBrainsPlugin({ outdir: './dist', excludeCss: true, packageJson: false, referencesTemplate: (_, tag) => { return { name: 'Documentation', url: `https://shoelace.style/components/${tag.replace('sl-', '')}` }; } }), customElementVuejsPlugin({ outdir: './dist/types/vue', fileName: 'index.d.ts', componentTypePath: (_, tag) => `../../components/${tag.replace('sl-', '')}/${tag.replace('sl-', '')}.component.js` }) ] }; ================================================ FILE: docs/_includes/component.njk ================================================ {% extends "default.njk" %} {# Find the component based on the `tag` front matter #} {% set component = getComponent('sl-' + page.fileSlug) %} {% block content %} {# Determine the badge variant #} {% if component.status == 'stable' %} {% set badgeVariant = 'primary' %} {% elseif component.status == 'experimental' %} {% set badgeVariant = 'warning' %} {% elseif component.status == 'planned' %} {% set badgeVariant = 'neutral' %} {% elseif component.status == 'deprecated' %} {% set badgeVariant = 'danger' %} {% else %} {% set badgeVariant = 'neutral' %} {% endif %} {# Header #}

{{ component.name | classNameToComponentName }}

<{{ component.tagName }}> | {{ component.name }}
Since {{component.since or '?' }} {{ component.status }}

{% if component.summary %} {{ component.summary | markdownInline | safe }} {% endif %}

{# Markdown content #} {{ content | safe }} {# Importing #}

Importing

If you're using the autoloader or the traditional loader, you can ignore this section. Otherwise, feel free to use any of the following snippets to cherry pick this component.

Script Import Bundler React

To import this component from the CDN using a script tag:

<script type="module" src="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{{ meta.version }}/{{ meta.cdndir }}/{{ component.path }}"></script>

To import this component from the CDN using a JavaScript import:

import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@{{ meta.version }}/{{ meta.cdndir }}/{{ component.path }}';

To import this component using a bundler:

import '@shoelace-style/shoelace/{{ meta.npmdir }}/{{ component.path }}';

To import this component as a React component:

import {{ component.name }} from '@shoelace-style/shoelace/{{ meta.npmdir }}/react/{{ component.tagNameWithoutPrefix }}';
{# Slots #} {% if component.slots.length %}

Slots

{% for slot in component.slots %} {% endfor %}
Name Description
{% if slot.name %} {{ slot.name }} {% else %} (default) {% endif %} {{ slot.description | markdownInline | safe }}

Learn more about using slots.

{% endif %} {# Properties #} {% if component.properties.length %}

Properties

{% for prop in component.properties %} {% endfor %}
Name Description Reflects Type Default
{{ prop.name }} {% if prop.attribute | length > 0 %} {% if prop.attribute != prop.name %}
{{ prop.attribute }} {% endif %} {% endif %}
{{ prop.description | markdownInline | safe }} {% if prop.reflects %} {% endif %} {% if prop.type.text %} {{ prop.type.text | trimPipes | markdownInline | safe }} {% else %} - {% endif %} {% if prop.default %} {{ prop.default | markdownInline | safe }} {% else %} - {% endif %}
updateComplete A read-only promise that resolves when the component has finished updating.

Learn more about attributes and properties.

{% endif %} {# Events #} {% if component.events.length %}

Events

{% for event in component.events %} {% endfor %}
Name React Event Description Event Detail
{{ event.name }} {{ event.reactName }} {{ event.description | markdownInline | safe }} {% if event.type.text %} {{ event.type.text | trimPipes }} {% else %} - {% endif %}

Learn more about events.

{% endif %} {# Methods #} {% if component.methods.length %}

Methods

{% for method in component.methods %} {% endfor %}
Name Description Arguments
{{ method.name }}() {{ method.description | markdownInline | safe }} {% if method.parameters.length %} {% for param in method.parameters %} {{ param.name }}: {{ param.type.text | trimPipes }}{% if not loop.last %},{% endif %} {% endfor %} {% else %} - {% endif %}

Learn more about methods.

{% endif %} {# Custom Properties #} {% if component.cssProperties.length %}

Custom Properties

{% for cssProperty in component.cssProperties %} {% endfor %}
Name Description Default
{{ cssProperty.name }} {{ cssProperty.description | markdownInline | safe }} {{ cssProperty.default }}

Learn more about customizing CSS custom properties.

{% endif %} {# CSS Parts #} {% if component.cssParts.length %}

Parts

{% for cssPart in component.cssParts %} {% endfor %}
Name Description
{{ cssPart.name }} {{ cssPart.description | markdownInline | safe }}

Learn more about customizing CSS parts.

{% endif %} {# Animations #} {% if component.animations.length %}

Animations

{% for animation in component.animations %} {% endfor %}
Name Description
{{ animation.name }} {{ animation.description | markdownInline | safe }}

Learn more about customizing animations.

{% endif %} {# Dependencies #} {% if component.dependencies.length %}

Dependencies

This component automatically imports the following dependencies.

{% endif %} {% endblock %} ================================================ FILE: docs/_includes/default.njk ================================================ {# Metadata #} {{ meta.title }} {# Opt out of Turbo caching #} {# Stylesheets #} {# Favicons #} {# Twitter Cards #} {# OpenGraph #} {# Shoelace #} {# Set the initial theme and menu states here to prevent flashing #} {# Turbo + Scroll positioning #} Skip to main content {# Menu toggle #} {# Icon toolbar #}
{# GitHub #} {# Twitter #} {# Theme selector #} Light Dark System
{% include 'wa-logo-icon.njk' %}

Shoelace is now Web Awesome!

Web Awesome has an even bigger library of free web components. Plus themes, utilities, patterns, and more!

Still open source. Now with more awesome.

Okay, got it
FYI, Shoelace is no longer actively being developed

But it is still available for use and may receive updates as needed.

{# Content #}
{% if toc %} {% endif %}
{% block content %} {{ content | safe }} {% endblock %}
================================================ FILE: docs/_includes/sidebar.njk ================================================ ================================================ FILE: docs/_includes/wa-logo-icon.njk ================================================ ================================================ FILE: docs/_utilities/active-links.cjs ================================================ function normalizePathname(pathname) { // Remove /index.html if (pathname.endsWith('/index.html')) { pathname = pathname.replace(/\/index\.html/, ''); } // Remove trailing slashes return pathname.replace(/\/$/, ''); } /** * Adds a class name to links that are currently active. */ module.exports = function (doc, options) { options = { className: 'active-link', // the class to add to active links pathname: undefined, // the current pathname to compare within: 'body', // element containing the target links ...options }; const within = doc.querySelector(options.within); if (!within) { return doc; } within.querySelectorAll('a').forEach(link => { if (normalizePathname(options.pathname) === normalizePathname(link.pathname)) { link.classList.add(options.className); } }); return doc; }; ================================================ FILE: docs/_utilities/anchor-headings.cjs ================================================ const { createSlug } = require('./strings.cjs'); /** * Turns headings into clickable, deep linkable anchors. The provided doc should be a document object provided by JSDOM. * The same document will be returned with the appropriate DOM manipulations. */ module.exports = function (doc, options) { options = { levels: ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'], // the headings to convert className: 'anchor-heading', // the class name to add within: 'body', // the element containing the target headings ...options }; const within = doc.querySelector(options.within); if (!within) { return doc; } within.querySelectorAll('h1, h2, h3, h4, h5, h6').forEach(heading => { const hasAnchor = heading.querySelector('a'); const anchor = doc.createElement('a'); let id = heading.textContent ?? ''; let suffix = 0; // Skip heading levels we don't care about if (!options.levels?.includes(heading.tagName.toLowerCase())) { return; } // Convert dots to underscores id = id.replace(/\./g, '_'); // Turn it into a slug id = createSlug(id); // Make sure it starts with a letter if (!/^[a-z]/i.test(id)) { id = `id_${id}`; } // Make sure the id is unique const originalId = id; while (doc.getElementById(id) !== null) { id = `${originalId}-${++suffix}`; } if (hasAnchor || !id) return; heading.setAttribute('id', id); anchor.setAttribute('href', `#${encodeURIComponent(id)}`); anchor.setAttribute('aria-label', `Direct link to "${heading.textContent}"`); if (options.className) { heading.classList.add(options.className); } // Append the anchor heading.append(anchor); }); return doc; }; ================================================ FILE: docs/_utilities/cem.cjs ================================================ const customElementsManifest = require('../../dist/custom-elements.json'); // // Export it here so we can import it elsewhere and use the same version // module.exports.customElementsManifest = customElementsManifest; // // Gets all components from custom-elements.json and returns them in a more documentation-friendly format. // module.exports.getAllComponents = function () { const allComponents = []; customElementsManifest.modules?.forEach(module => { module.declarations?.forEach(declaration => { if (declaration.customElement) { // Generate the dist path based on the src path and attach it to the component declaration.path = module.path.replace(/^src\//, 'dist/').replace(/\.ts$/, '.js'); // Remove members that are private or don't have a description const members = declaration.members?.filter(member => member.description && member.privacy !== 'private'); const methods = members?.filter(prop => prop.kind === 'method' && prop.privacy !== 'private'); const properties = members?.filter(prop => { // Look for a corresponding attribute const attribute = declaration.attributes?.find(attr => attr.fieldName === prop.name); if (attribute) { prop.attribute = attribute.name || attribute.fieldName; } return prop.kind === 'field' && prop.privacy !== 'private'; }); allComponents.push({ ...declaration, methods, properties }); } }); }); // Build dependency graphs allComponents.forEach(component => { const dependencies = []; // Recursively fetch sub-dependencies function getDependencies(tag) { const cmp = allComponents.find(c => c.tagName === tag); if (!cmp || !Array.isArray(component.dependencies)) { return; } cmp.dependencies?.forEach(dependentTag => { if (!dependencies.includes(dependentTag)) { dependencies.push(dependentTag); } getDependencies(dependentTag); }); } getDependencies(component.tagName); component.dependencies = dependencies.sort(); }); // Sort by name return allComponents.sort((a, b) => { if (a.name < b.name) return -1; if (a.name > b.name) return 1; return 0; }); }; ================================================ FILE: docs/_utilities/code-previews.cjs ================================================ let count = 1; function escapeHtml(str) { return String(str).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } /** * Turns code fields with the :preview suffix into interactive code previews. */ module.exports = function (doc, options) { options = { within: 'body', // the element containing the code fields to convert ...options }; const within = doc.querySelector(options.within); if (!within) { return doc; } within.querySelectorAll('[class*=":preview"]').forEach(code => { const pre = code.closest('pre'); if (!pre) { return; } const adjacentPre = pre.nextElementSibling?.tagName.toLowerCase() === 'pre' ? pre.nextElementSibling : null; const reactCode = adjacentPre?.querySelector('code[class$="react"]'); const sourceGroupId = `code-preview-source-group-${count}`; const isExpanded = code.getAttribute('class').includes(':expanded'); const noCodePen = code.getAttribute('class').includes(':no-codepen'); count++; const htmlButton = ` `; const reactButton = ` `; const codePenButton = ` `; const codePreview = `
${code.textContent}
${escapeHtml(code.textContent)}
${ reactCode ? `
${escapeHtml(reactCode.textContent)}
` : '' }
${reactCode ? ` ${htmlButton} ${reactButton} ` : ''} ${noCodePen ? '' : codePenButton}
`; pre.insertAdjacentHTML('afterend', codePreview); pre.remove(); if (adjacentPre) { adjacentPre.remove(); } }); // Wrap code preview scripts in anonymous functions so they don't run in the global scope doc.querySelectorAll('.code-preview__preview script').forEach(script => { if (script.type === 'module') { // Modules are already scoped script.textContent = script.innerHTML; } else { // Wrap non-modules in an anonymous function so they don't run in the global scope script.textContent = `(() => { ${script.innerHTML} })();`; } }); return doc; }; ================================================ FILE: docs/_utilities/copy-code-buttons.cjs ================================================ let codeBlockId = 0; /** * Adds copy code buttons to code fields. The provided doc should be a document object provided by JSDOM. The same * document will be returned with the appropriate DOM manipulations. */ module.exports = function (doc) { doc.querySelectorAll('pre > code').forEach(code => { const pre = code.closest('pre'); const button = doc.createElement('sl-copy-button'); if (!code.id) { code.id = `code-block-${++codeBlockId}`; } button.classList.add('copy-code-button'); button.setAttribute('from', code.id); pre.append(button); }); return doc; }; ================================================ FILE: docs/_utilities/external-links.cjs ================================================ const { isExternalLink } = require('./strings.cjs'); /** * Transforms external links to make them safer and optionally add a target. The provided doc should be a document * object provided by JSDOM. The same document will be returned with the appropriate DOM manipulations. */ module.exports = function (doc, options) { options = { className: 'external-link', // the class name to add to links noopener: true, // sets rel="noopener" noreferrer: true, // sets rel="noreferrer" ignore: () => false, // callback function to filter links that should be ignored within: 'body', // element that contains the target links target: '', // sets the target attribute ...options }; const within = doc.querySelector(options.within); if (within) { within.querySelectorAll('a').forEach(link => { if (isExternalLink(link) && !options.ignore(link)) { link.classList.add(options.className); const rel = []; if (options.noopener) rel.push('noopener'); if (options.noreferrer) rel.push('noreferrer'); if (rel.length) { link.setAttribute('rel', rel.join(' ')); } if (options.target) { link.setAttribute('target', options.target); } } }); } return doc; }; ================================================ FILE: docs/_utilities/highlight-code.cjs ================================================ const Prism = require('prismjs'); const PrismLoader = require('prismjs/components/index.js'); PrismLoader('diff'); PrismLoader.silent = true; /** Highlights a code string. */ function highlight(code, language) { const alias = language.replace(/^diff-/, ''); const isDiff = /^diff-/i.test(language); // Auto-load the target language if (!Prism.languages[alias]) { PrismLoader(alias); if (!Prism.languages[alias]) { throw new Error(`Unsupported language for code highlighting: "${language}"`); } } // Register diff-* languages to use the diff grammar if (isDiff) { Prism.languages[language] = Prism.languages.diff; } return Prism.highlight(code, Prism.languages[language], language); } /** * Highlights all code fields that have a language parameter. If the language has a colon in its name, the first chunk * will be the language used and additional chunks will be applied as classes to the `
`. For example, a code field
 * tagged with "html:preview" will be rendered as `
`.
 *
 * The provided doc should be a document object provided by JSDOM. The same document will be returned with the
 * appropriate DOM manipulations.
 */
module.exports = function (doc) {
  doc.querySelectorAll('pre > code[class]').forEach(code => {
    // Look for class="language-*" and split colons into separate classes
    code.classList.forEach(className => {
      if (className.startsWith('language-')) {
        //
        // We use certain suffixes to indicate code previews, expanded states, etc. The class might look something like
        // this:
        //
        //  class="language-html:preview:expanded"
        //
        // The language will always come first, so we need to drop the "language-" prefix and everything after the first
        // color to get the highlighter language.
        //
        const language = className.replace(/^language-/, '').split(':')[0];

        try {
          code.innerHTML = highlight(code.textContent ?? '', language);
        } catch (err) {
          // Language not found, skip it
        }
      }
    });
  });

  return doc;
};


================================================
FILE: docs/_utilities/markdown.cjs
================================================
const MarkdownIt = require('markdown-it');
const markdownItContainer = require('markdown-it-container');
const markdownItIns = require('markdown-it-ins');
const markdownItKbd = require('markdown-it-kbd');
const markdownItMark = require('markdown-it-mark');
const markdownItReplaceIt = require('markdown-it-replace-it');

const markdown = MarkdownIt({
  html: true,
  xhtmlOut: false,
  breaks: false,
  langPrefix: 'language-',
  linkify: false,
  typographer: false
});

// Third-party plugins
markdown.use(markdownItContainer);
markdown.use(markdownItIns);
markdown.use(markdownItKbd);
markdown.use(markdownItMark);
markdown.use(markdownItReplaceIt);

// Callouts
['tip', 'warning', 'danger'].forEach(type => {
  markdown.use(markdownItContainer, type, {
    render: function (tokens, idx) {
      if (tokens[idx].nesting === 1) {
        return `\n';
    }
  });
});

// Asides
markdown.use(markdownItContainer, 'aside', {
  render: function (tokens, idx) {
    if (tokens[idx].nesting === 1) {
      return `\n';
  }
});

// Details
markdown.use(markdownItContainer, 'details', {
  validate: params => params.trim().match(/^details\s+(.*)$/),
  render: (tokens, idx) => {
    const m = tokens[idx].info.trim().match(/^details\s+(.*)$/);
    if (tokens[idx].nesting === 1) {
      return `
\n${markdown.utils.escapeHtml(m[1])}\n`; } return '
\n'; } }); // Replace [#1234] with a link to GitHub issues markdownItReplaceIt.replacements.push({ name: 'github-issues', re: /\[#([0-9]+)\]/gs, sub: '#$1', html: true, default: true }); module.exports = markdown; ================================================ FILE: docs/_utilities/prettier.cjs ================================================ const { format } = require('prettier'); /** Formats markup using prettier. */ module.exports = function (content, options) { options = { arrowParens: 'avoid', bracketSpacing: true, htmlWhitespaceSensitivity: 'css', insertPragma: false, bracketSameLine: false, jsxSingleQuote: false, parser: 'html', printWidth: 120, proseWrap: 'preserve', quoteProps: 'as-needed', requirePragma: false, semi: true, singleQuote: true, tabWidth: 2, trailingComma: 'none', useTabs: false, ...options }; return format(content, options); }; ================================================ FILE: docs/_utilities/replacer.cjs ================================================ /** * @typedef {object} Replacement * @property {string | RegExp} pattern * @property {string} replacement */ /** * @typedef {Array} Replacements */ /** * @param {String} rawContent * @param {Replacements} replacements */ module.exports = function (rawContent, replacements) { let content = rawContent; replacements.forEach(replacement => { content = content.replaceAll(replacement.pattern, replacement.replacement); }); return content; }; ================================================ FILE: docs/_utilities/scrolling-tables.cjs ================================================ /** * Turns headings into clickable, deep linkable anchors. The provided doc should be a document object provided by JSDOM. * The same document will be returned with the appropriate DOM manipulations. */ module.exports = function (doc, options) { const tables = [...doc.querySelectorAll('table')]; options = { className: 'table-scroll', // the class name to add to the table's container ...options }; tables.forEach(table => { const div = doc.createElement('div'); div.classList.add(options.className); table.insertAdjacentElement('beforebegin', div); div.append(table); }); return doc; }; ================================================ FILE: docs/_utilities/strings.cjs ================================================ const slugify = require('slugify'); /** Creates a slug from an arbitrary string of text. */ module.exports.createSlug = function (text) { return slugify(String(text), { remove: /[^\w|\s]/g, lower: true }); }; /** Determines whether or not a link is external. */ module.exports.isExternalLink = function (link) { // We use the "internal" hostname when initializing JSDOM so we know that those are local links if (!link.hostname || link.hostname === 'internal') return false; return true; }; ================================================ FILE: docs/_utilities/table-of-contents.cjs ================================================ /** * Generates an in-page table of contents based on headings. */ module.exports = function (doc, options) { options = { levels: ['h2'], // headings to include (they must have an id) container: 'nav', // the container to append links to listItem: true, // if true, links will be wrapped in
  • within: 'body', // the element containing the headings to summarize ...options }; const container = doc.querySelector(options.container); const within = doc.querySelector(options.within); const headingSelector = options.levels.map(h => `${h}[id]`).join(', '); if (!container || !within) { return doc; } within.querySelectorAll(headingSelector).forEach(heading => { const listItem = doc.createElement('li'); const link = doc.createElement('a'); const level = heading.tagName.slice(1); link.href = `#${heading.id}`; link.textContent = heading.textContent; if (options.listItem) { // List item + link listItem.setAttribute('data-level', level); listItem.append(link); container.append(listItem); } else { // Link only link.setAttribute('data-level', level); container.append(link); } }); return doc; }; ================================================ FILE: docs/_utilities/typography.cjs ================================================ const smartquotes = require('smartquotes'); smartquotes.replacements.push([/---/g, '\u2014']); // em dash smartquotes.replacements.push([/--/g, '\u2013']); // en dash smartquotes.replacements.push([/\.\.\./g, '\u2026']); // ellipsis smartquotes.replacements.push([/\(c\)/gi, '\u00A9']); // copyright smartquotes.replacements.push([/\(r\)/gi, '\u00AE']); // registered trademark smartquotes.replacements.push([/\?!/g, '\u2048']); // ?! smartquotes.replacements.push([/!!/g, '\u203C']); // !! smartquotes.replacements.push([/\?\?/g, '\u2047']); // ?? smartquotes.replacements.push([/([0-9]\s?)-(\s?[0-9])/g, '$1\u2013$2']); // number ranges use en dash /** * Improves typography by adding smart quotes and similar corrections within the specified element(s). * * The provided doc should be a document object provided by JSDOM. The same document will be returned with the * appropriate DOM manipulations. */ module.exports = function (doc, selector = 'body') { const elements = [...doc.querySelectorAll(selector)]; elements.forEach(el => smartquotes.element(el)); return doc; }; ================================================ FILE: docs/assets/examples/include.html ================================================

    The content in this example was included from a separate file. 🤯

    Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lectus vestibulum mattis ullamcorper velit sed ullamcorper morbi. Fringilla urna porttitor rhoncus dolor purus non enim. Nullam vehicula ipsum a arcu cursus vitae congue mauris. Gravida in fermentum et sollicitudin.

    Cursus sit amet dictum sit amet justo donec enim. Sed id semper risus in hendrerit gravida. Viverra accumsan in nisl nisi scelerisque eu ultrices vitae. Et molestie ac feugiat sed lectus vestibulum mattis ullamcorper velit. Nec ullamcorper sit amet risus nullam. Et egestas quis ipsum suspendisse ultrices gravida dictum. Lorem donec massa sapien faucibus et molestie. A cras semper auctor neque vitae.

    ================================================ FILE: docs/assets/scripts/code-previews.js ================================================ (() => { function convertModuleLinks(html) { html = html .replace(/@shoelace-style\/shoelace/g, `https://esm.sh/@shoelace-style/shoelace@${shoelaceVersion}`) .replace(/from 'react'/g, `from 'https://esm.sh/react@${reactVersion}'`) .replace(/from "react"/g, `from "https://esm.sh/react@${reactVersion}"`); return html; } function getAdjacentExample(name, pre) { let currentPre = pre.nextElementSibling; while (currentPre?.tagName.toLowerCase() === 'pre') { if (currentPre?.getAttribute('data-lang').split(' ').includes(name)) { return currentPre; } currentPre = currentPre.nextElementSibling; } return null; } function runScript(script) { const newScript = document.createElement('script'); if (script.type === 'module') { newScript.type = 'module'; newScript.textContent = script.innerHTML; } else { newScript.appendChild(document.createTextNode(`(() => { ${script.innerHTML} })();`)); } script.parentNode.replaceChild(newScript, script); } function getFlavor() { return sessionStorage.getItem('flavor') || 'html'; } function setFlavor(newFlavor) { flavor = ['html', 'react'].includes(newFlavor) ? newFlavor : 'html'; sessionStorage.setItem('flavor', flavor); // Set the flavor class on the body document.documentElement.classList.toggle('flavor-html', flavor === 'html'); document.documentElement.classList.toggle('flavor-react', flavor === 'react'); } function syncFlavor() { setFlavor(getFlavor()); document.querySelectorAll('.code-preview__button--html').forEach(preview => { if (flavor === 'html') { preview.classList.add('code-preview__button--selected'); } }); document.querySelectorAll('.code-preview__button--react').forEach(preview => { if (flavor === 'react') { preview.classList.add('code-preview__button--selected'); } }); } const shoelaceVersion = document.documentElement.getAttribute('data-shoelace-version'); const reactVersion = '^18'; const cdndir = 'cdn'; const npmdir = 'dist'; let flavor = getFlavor(); let count = 1; // We need the version to open if (!shoelaceVersion) { throw new Error('The data-shoelace-version attribute is missing from .'); } // Sync flavor UI on page load syncFlavor(); // // Resizing previews // document.addEventListener('mousedown', handleResizerDrag); document.addEventListener('touchstart', handleResizerDrag, { passive: true }); function handleResizerDrag(event) { const resizer = event.target.closest('.code-preview__resizer'); const preview = event.target.closest('.code-preview__preview'); if (!resizer || !preview) return; let startX = event.changedTouches ? event.changedTouches[0].pageX : event.clientX; let startWidth = parseInt(document.defaultView.getComputedStyle(preview).width, 10); event.preventDefault(); preview.classList.add('code-preview__preview--dragging'); document.documentElement.addEventListener('mousemove', dragMove); document.documentElement.addEventListener('touchmove', dragMove); document.documentElement.addEventListener('mouseup', dragStop); document.documentElement.addEventListener('touchend', dragStop); function dragMove(event) { const width = startWidth + (event.changedTouches ? event.changedTouches[0].pageX : event.pageX) - startX; preview.style.width = `${width}px`; } function dragStop() { preview.classList.remove('code-preview__preview--dragging'); document.documentElement.removeEventListener('mousemove', dragMove); document.documentElement.removeEventListener('touchmove', dragMove); document.documentElement.removeEventListener('mouseup', dragStop); document.documentElement.removeEventListener('touchend', dragStop); } } // // Toggle source mode // document.addEventListener('click', event => { const button = event.target.closest('.code-preview__button'); const codeBlock = button?.closest('.code-preview'); if (button?.classList.contains('code-preview__button--html')) { // Show HTML setFlavor('html'); toggleSource(codeBlock, true); } else if (button?.classList.contains('code-preview__button--react')) { // Show React setFlavor('react'); toggleSource(codeBlock, true); } else if (button?.classList.contains('code-preview__toggle')) { // Toggle source toggleSource(codeBlock); } else { return; } // Update flavor buttons [...document.querySelectorAll('.code-preview')].forEach(cb => { cb.querySelector('.code-preview__button--html')?.classList.toggle( 'code-preview__button--selected', flavor === 'html' ); cb.querySelector('.code-preview__button--react')?.classList.toggle( 'code-preview__button--selected', flavor === 'react' ); }); }); function toggleSource(codeBlock, force) { codeBlock.classList.toggle('code-preview--expanded', force); event.target.setAttribute('aria-expanded', codeBlock.classList.contains('code-preview--expanded')); } // // Open in CodePen // document.addEventListener('click', event => { const button = event.target.closest('button'); if (button?.classList.contains('code-preview__button--codepen')) { const codeBlock = button.closest('.code-preview'); const htmlExample = codeBlock.querySelector('.code-preview__source--html > pre > code')?.textContent; const reactExample = codeBlock.querySelector('.code-preview__source--react > pre > code')?.textContent; const isReact = flavor === 'react' && typeof reactExample === 'string'; const theme = document.documentElement.classList.contains('sl-theme-dark') ? 'dark' : 'light'; const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches; const isDark = theme === 'dark' || (theme === 'auto' && prefersDark); const editors = isReact ? '0010' : '1000'; let htmlTemplate = ''; let jsTemplate = ''; let cssTemplate = ''; const form = document.createElement('form'); form.action = 'https://codepen.io/pen/define'; form.method = 'POST'; form.target = '_blank'; // HTML templates if (!isReact) { htmlTemplate = `\n` + `\n${htmlExample}`; jsTemplate = ''; } // React templates if (isReact) { htmlTemplate = '
    '; jsTemplate = `import React from 'https://esm.sh/react@${reactVersion}';\n` + `import ReactDOM from 'https://esm.sh/react-dom@${reactVersion}';\n` + `import { setBasePath } from 'https://esm.sh/@shoelace-style/shoelace@${shoelaceVersion}/${cdndir}/utilities/base-path';\n` + `\n` + `// Set the base path for Shoelace assets\n` + `setBasePath('https://esm.sh/@shoelace-style/shoelace@${shoelaceVersion}/${npmdir}/')\n` + `\n${convertModuleLinks(reactExample)}\n` + `\n` + `ReactDOM.render(, document.getElementById('root'));`; } // CSS templates cssTemplate = `@import 'https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@${shoelaceVersion}/${cdndir}/themes/${ isDark ? 'dark' : 'light' }.css';\n` + '\n' + 'body {\n' + ' font: 16px sans-serif;\n' + ' background-color: var(--sl-color-neutral-0);\n' + ' color: var(--sl-color-neutral-900);\n' + ' padding: 1rem;\n' + '}'; // Docs: https://blog.codepen.io/documentation/prefill/ const data = { title: '', description: '', tags: ['shoelace', 'web components'], editors, head: ``, html_classes: `sl-theme-${isDark ? 'dark' : 'light'}`, css_external: ``, js_external: ``, js_module: true, js_pre_processor: isReact ? 'babel' : 'none', html: htmlTemplate, css: cssTemplate, js: jsTemplate }; const input = document.createElement('input'); input.type = 'hidden'; input.name = 'data'; input.value = JSON.stringify(data); form.append(input); document.documentElement.append(form); form.submit(); form.remove(); } }); // Set the initial flavor window.addEventListener('turbo:load', syncFlavor); })(); ================================================ FILE: docs/assets/scripts/docs.js ================================================ // // Sidebar // // When the sidebar is hidden, we apply the inert attribute to prevent focus from reaching it. Due to the many states // the sidebar can have (e.g. static, hidden, expanded), we test for visibility by checking to see if it's placed // offscreen or not. Then, on resize/transition we make sure to update the attribute accordingly. // (() => { function getSidebar() { return document.getElementById('sidebar'); } function isSidebarOpen() { return document.documentElement.classList.contains('sidebar-open'); } function isSidebarVisible() { return getSidebar().getBoundingClientRect().x >= 0; } function toggleSidebar(force) { const isOpen = typeof force === 'boolean' ? force : !isSidebarOpen(); return document.documentElement.classList.toggle('sidebar-open', isOpen); } function updateInert() { getSidebar().inert = !isSidebarVisible(); } // Toggle the menu document.addEventListener('click', event => { const menuToggle = event.target.closest('#menu-toggle'); if (!menuToggle) return; toggleSidebar(); }); // Update the sidebar's inert state when the window resizes and when the sidebar transitions window.addEventListener('resize', () => toggleSidebar(false)); document.addEventListener('transitionend', event => { const sidebar = event.target.closest('#sidebar'); if (!sidebar) return; updateInert(); }); // Close when a menu item is selected on mobile document.addEventListener('click', event => { const sidebar = event.target.closest('#sidebar'); const link = event.target.closest('a'); if (!sidebar || !link) return; if (isSidebarOpen()) { toggleSidebar(); } }); // Close when open and escape is pressed document.addEventListener('keydown', event => { if (event.key === 'Escape' && isSidebarOpen()) { event.stopImmediatePropagation(); toggleSidebar(); } }); // Close when clicking outside of the sidebar document.addEventListener('mousedown', event => { if (isSidebarOpen() & !event.target?.closest('#sidebar, #menu-toggle')) { event.stopImmediatePropagation(); toggleSidebar(); } }); updateInert(); })(); // // Theme selector // (() => { function getTheme() { return localStorage.getItem('theme') || 'auto'; } function isDark() { if (theme === 'auto') { return window.matchMedia('(prefers-color-scheme: dark)').matches; } return theme === 'dark'; } function setTheme(newTheme) { theme = newTheme; localStorage.setItem('theme', theme); // Update the UI updateSelection(); // Toggle the dark mode class document.documentElement.classList.toggle('sl-theme-dark', isDark()); } function updateSelection() { const menu = document.querySelector('#theme-selector sl-menu'); if (!menu) return; [...menu.querySelectorAll('sl-menu-item')].map(item => (item.checked = item.getAttribute('value') === theme)); } let theme = getTheme(); // Selection is not preserved when changing page, so update when opening dropdown document.addEventListener('sl-show', event => { const themeSelector = event.target.closest('#theme-selector'); if (!themeSelector) return; updateSelection(); }); // Listen for selections document.addEventListener('sl-select', event => { const menu = event.target.closest('#theme-selector sl-menu'); if (!menu) return; setTheme(event.detail.item.value); }); // Update the theme when the preference changes window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => setTheme(theme)); // Toggle with backslash document.addEventListener('keydown', event => { if ( event.key === '\\' && !event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase())) ) { event.preventDefault(); setTheme(isDark() ? 'light' : 'dark'); } }); // Set the initial theme and sync the UI setTheme(theme); })(); // // Open details when printing // (() => { const detailsOpenOnPrint = new Set(); window.addEventListener('beforeprint', () => { detailsOpenOnPrint.clear(); document.querySelectorAll('details').forEach(details => { if (details.open) { detailsOpenOnPrint.add(details); } details.open = true; }); }); window.addEventListener('afterprint', () => { document.querySelectorAll('details').forEach(details => { details.open = detailsOpenOnPrint.has(details); }); detailsOpenOnPrint.clear(); }); })(); // // Smooth links // (() => { document.addEventListener('click', event => { const link = event.target.closest('a'); const id = (link?.hash ?? '').substr(1); const isFragment = link?.hasAttribute('href') && link?.getAttribute('href').startsWith('#'); if (!link || !isFragment || link.getAttribute('data-smooth-link') === 'false') { return; } // Scroll to the top if (link.hash === '') { event.preventDefault(); window.scroll({ top: 0, behavior: 'smooth' }); history.pushState(undefined, undefined, location.pathname); } // Scroll to an id if (id) { const target = document.getElementById(id); if (target) { event.preventDefault(); window.scroll({ top: target.offsetTop, behavior: 'smooth' }); history.pushState(undefined, undefined, `#${id}`); } } }); })(); // // Table of Contents scrollspy // (() => { // This will be stale if its not a function. const getLinks = () => [...document.querySelectorAll('.content__toc a')]; const linkTargets = new WeakMap(); const visibleTargets = new WeakSet(); const observer = new IntersectionObserver(handleIntersect, { rootMargin: '0px 0px' }); let debounce; function handleIntersect(entries) { entries.forEach(entry => { // Remember which targets are visible if (entry.isIntersecting) { visibleTargets.add(entry.target); } else { visibleTargets.delete(entry.target); } }); updateActiveLinks(); } function updateActiveLinks() { const links = getLinks(); // Find the first visible target and activate the respective link links.find(link => { const target = linkTargets.get(link); if (target && visibleTargets.has(target)) { links.forEach(el => el.classList.toggle('active', el === link)); return true; } return false; }); } // Observe link targets function observeLinks() { getLinks().forEach(link => { const hash = link.hash.slice(1); const target = hash ? document.querySelector(`.content__body #${hash}`) : null; if (target) { linkTargets.set(link, target); observer.observe(target); } }); } observeLinks(); document.addEventListener('turbo:load', updateActiveLinks); document.addEventListener('turbo:load', observeLinks); })(); // // Show custom versions in the sidebar // (() => { function updateVersion() { const el = document.querySelector('.sidebar-version'); if (!el) return; if (location.hostname === 'next.shoelace.style') el.textContent = 'Next'; if (location.hostname === 'localhost') el.textContent = 'Development'; } updateVersion(); document.addEventListener('turbo:load', updateVersion); })(); ================================================ FILE: docs/assets/scripts/search.js ================================================ (() => { // Append the search dialog to the body const siteSearch = document.createElement('div'); const scrollbarWidth = Math.abs(window.innerWidth - document.documentElement.clientWidth); siteSearch.classList.add('search'); siteSearch.innerHTML = `
      No matching pages
      Navigate Select Esc Close
      `; const overlay = siteSearch.querySelector('.search__overlay'); const dialog = siteSearch.querySelector('.search__dialog'); const input = siteSearch.querySelector('.search__input'); const clearButton = siteSearch.querySelector('.search__clear-button'); const results = siteSearch.querySelector('.search__results'); const version = document.documentElement.getAttribute('data-shoelace-version'); const key = `search_${version}`; const searchDebounce = 50; const animationDuration = 150; let isShowing = false; let searchTimeout; let searchIndex; let map; const loadSearchIndex = new Promise(resolve => { const cache = localStorage.getItem(key); const wait = 'requestIdleCallback' in window ? requestIdleCallback : requestAnimationFrame; // Cleanup older search indices (everything before this version) try { const items = { ...localStorage }; Object.keys(items).forEach(k => { if (key > k) { localStorage.removeItem(k); } }); } catch { /* do nothing */ } // Look for a cached index try { if (cache) { const data = JSON.parse(cache); searchIndex = window.lunr.Index.load(data.searchIndex); map = data.map; return resolve(); } } catch { /* do nothing */ } // Wait until idle to fetch the index wait(() => { fetch('/assets/search.json') .then(res => res.json()) .then(data => { if (!window.lunr) { console.error('The Lunr search client has not yet been loaded.'); } searchIndex = window.lunr.Index.load(data.searchIndex); map = data.map; // Cache the search index for this version if (version) { try { localStorage.setItem(key, JSON.stringify(data)); } catch (err) { console.warn(`Unable to cache the search index: ${err}`); } } resolve(); }); }); }); async function show() { isShowing = true; document.body.append(siteSearch); document.body.classList.add('search-visible'); document.body.style.setProperty('--docs-search-scroll-lock-size', `${scrollbarWidth}px`); clearButton.hidden = true; requestAnimationFrame(() => input.focus()); updateResults(); dialog.showModal(); await Promise.all([ dialog.animate( [ { opacity: 0, transform: 'scale(.9)', transformOrigin: 'top' }, { opacity: 1, transform: 'scale(1)', transformOrigin: 'top' } ], { duration: animationDuration } ).finished, overlay.animate([{ opacity: 0 }, { opacity: 1 }], { duration: animationDuration }).finished ]); dialog.addEventListener('mousedown', handleMouseDown); dialog.addEventListener('keydown', handleKeyDown); } async function hide() { isShowing = false; await Promise.all([ dialog.animate( [ { opacity: 1, transform: 'scale(1)', transformOrigin: 'top' }, { opacity: 0, transform: 'scale(.9)', transformOrigin: 'top' } ], { duration: animationDuration } ).finished, overlay.animate([{ opacity: 1 }, { opacity: 0 }], { duration: animationDuration }).finished ]); dialog.close(); input.blur(); // otherwise Safari will scroll to the bottom of the page on close input.value = ''; document.body.classList.remove('search-visible'); document.body.style.removeProperty('--docs-search-scroll-lock-size'); siteSearch.remove(); updateResults(); dialog.removeEventListener('mousedown', handleMouseDown); dialog.removeEventListener('keydown', handleKeyDown); } function handleInput() { clearButton.hidden = input.value === ''; // Debounce search queries clearTimeout(searchTimeout); searchTimeout = setTimeout(() => updateResults(input.value), searchDebounce); } function handleClear() { clearButton.hidden = true; input.value = ''; input.focus(); updateResults(); } function handleMouseDown(event) { if (!event.target.closest('.search__content')) { hide(); } } function handleKeyDown(event) { // Close when pressing escape if (event.key === 'Escape') { event.preventDefault(); // prevent from closing immediately so it can animate event.stopImmediatePropagation(); hide(); return; } // Handle keyboard selections if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) { event.preventDefault(); const currentEl = results.querySelector('[data-selected="true"]'); const items = [...results.querySelectorAll('li')]; const index = items.indexOf(currentEl); let nextEl; if (items.length === 0) { return; } switch (event.key) { case 'ArrowUp': nextEl = items[Math.max(0, index - 1)]; break; case 'ArrowDown': nextEl = items[Math.min(items.length - 1, index + 1)]; break; case 'Home': nextEl = items[0]; break; case 'End': nextEl = items[items.length - 1]; break; case 'Enter': currentEl?.querySelector('a')?.click(); break; } // Update the selected item items.forEach(item => { if (item === nextEl) { input.setAttribute('aria-activedescendant', item.id); item.setAttribute('data-selected', 'true'); nextEl.scrollIntoView({ block: 'nearest' }); } else { item.setAttribute('data-selected', 'false'); } }); } } async function updateResults(query = '') { try { await loadSearchIndex; const hasQuery = query.length > 0; const searchTerms = query .split(' ') .map((term, index, arr) => { // Search API: https://lunrjs.com/guides/searching.html if (index === arr.length - 1) { // The last term is not mandatory and 1x fuzzy. We also duplicate it with a wildcard to match partial words // as the user types. return `${term}~1 ${term}*`; } else { // All other terms are mandatory and 1x fuzzy return `+${term}~1`; } }) .join(' '); const matches = hasQuery ? searchIndex.search(searchTerms) : []; const hasResults = hasQuery && matches.length > 0; siteSearch.classList.toggle('search--has-results', hasQuery && hasResults); siteSearch.classList.toggle('search--no-results', hasQuery && !hasResults); input.setAttribute('aria-activedescendant', ''); results.innerHTML = ''; matches.forEach((match, index) => { const page = map[match.ref]; const li = document.createElement('li'); const a = document.createElement('a'); const displayTitle = page.title ?? ''; const displayDescription = page.description ?? ''; const displayUrl = page.url.replace(/^\//, '').replace(/\/$/, ''); let icon = 'file-text'; a.setAttribute('role', 'option'); a.setAttribute('id', `search-result-item-${match.ref}`); if (page.url.includes('getting-started/')) { icon = 'lightbulb'; } if (page.url.includes('resources/')) { icon = 'book'; } if (page.url.includes('components/')) { icon = 'puzzle'; } if (page.url.includes('tokens/')) { icon = 'palette2'; } if (page.url.includes('utilities/')) { icon = 'wrench'; } if (page.url.includes('tutorials/')) { icon = 'joystick'; } li.classList.add('search__result'); li.setAttribute('role', 'option'); li.setAttribute('id', `search-result-item-${match.ref}`); li.setAttribute('data-selected', index === 0 ? 'true' : 'false'); a.href = page.url; a.innerHTML = `
      `; a.querySelector('.search__result-title').textContent = displayTitle; a.querySelector('.search__result-description').textContent = displayDescription; a.querySelector('.search__result-url').textContent = displayUrl; li.appendChild(a); results.appendChild(li); }); } catch { // Ignore query errors as the user types } } // Show the search dialog when clicking on data-plugin="search" document.addEventListener('click', event => { const searchButton = event.target.closest('[data-plugin="search"]'); if (searchButton) { show(); } }); // Show the search dialog when slash (or CMD+K) is pressed and focus is not inside a form element document.addEventListener('keydown', event => { if ( !isShowing && (event.key === '/' || (event.key === 'k' && (event.metaKey || event.ctrlKey))) && !event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase())) ) { event.preventDefault(); show(); } }); // Purge cache when we press CMD+CTRL+R document.addEventListener('keydown', event => { if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'r') { localStorage.clear(); } }); input.addEventListener('input', handleInput); clearButton.addEventListener('click', handleClear); // Close when a result is selected results.addEventListener('click', event => { if (event.target.closest('a')) { hide(); } }); // We're using Turbo, so when a user searches for something, visits a result, and presses the back button, the search // UI will still be visible but not interactive. This removes the search UI when Turbo renders a page so they don't // get trapped. window.addEventListener('turbo:render', () => { document.body.classList.remove('search-visible'); document.querySelectorAll('.search__overlay, .search__dialog').forEach(el => el.remove()); }); })(); ================================================ FILE: docs/assets/scripts/turbo.js ================================================ import * as Turbo from 'https://cdn.jsdelivr.net/npm/@hotwired/turbo@8.0.10/+esm'; (() => { if (!window.scrollPositions) { window.scrollPositions = {}; } function preserveScroll() { document.querySelectorAll('[data-preserve-scroll]').forEach(element => { scrollPositions[element.id] = element.scrollTop; }); } function restoreScroll(event) { document.querySelectorAll('[data-preserve-scroll]').forEach(element => { element.scrollTop = scrollPositions[element.id]; }); if (event.detail && event.detail.newBody) { event.detail.newBody.querySelectorAll('[data-preserve-scroll').forEach(element => { element.scrollTop = scrollPositions[element.id]; }); } } window.addEventListener('turbo:before-cache', preserveScroll); window.addEventListener('turbo:before-render', restoreScroll); window.addEventListener('turbo:render', restoreScroll); })(); ================================================ FILE: docs/assets/styles/code-previews.css ================================================ /* Interactive code blocks */ .code-preview { position: relative; border-radius: 3px; background-color: var(--sl-color-neutral-50); margin-bottom: 1.5rem; } .code-preview__preview { position: relative; border: solid 1px var(--sl-color-neutral-200); border-bottom: none; border-top-left-radius: 3px; border-top-right-radius: 3px; background-color: var(--sl-color-neutral-0); min-width: 20rem; max-width: 100%; padding: 1.5rem 3.25rem 1.5rem 1.5rem; } /* Block the preview while dragging to prevent iframes from intercepting drag events */ .code-preview__preview--dragging:after { content: ''; position: absolute; top: 0; left: 0; width: 100%; height: 100%; opacity: 0; cursor: ew-resize; } .code-preview__resizer { display: flex; align-items: center; justify-content: center; position: absolute; top: 0; right: 0; bottom: 0; width: 1.75rem; font-size: 20px; color: var(--sl-color-neutral-600); background-color: var(--sl-color-neutral-0); border-left: solid 1px var(--sl-color-neutral-200); border-top-right-radius: 3px; cursor: ew-resize; } @media screen and (max-width: 600px) { .code-preview__preview { padding-right: 1.5rem; } .code-preview__resizer { display: none; } } .code-preview__source { border: solid 1px var(--sl-color-neutral-200); border-bottom: none; border-radius: 0 !important; display: none; } .code-preview--expanded .code-preview__source { display: block; } .code-preview__source pre { margin: 0; } .code-preview__buttons { position: relative; border: solid 1px var(--sl-color-neutral-200); border-bottom-left-radius: 3px; border-bottom-right-radius: 3px; display: flex; } .code-preview__button { flex: 0 0 auto; height: 2.5rem; min-width: 2.5rem; border: none; border-radius: 0; background: var(--sl-color-neutral-0); font: inherit; font-size: 0.7rem; font-weight: 500; text-transform: uppercase; color: var(--sl-color-neutral-600); padding: 0 1rem; cursor: pointer; } .code-preview__button:not(:last-of-type) { border-right: solid 1px var(--sl-color-neutral-200); } .code-preview__button--html, .code-preview__button--react { width: 70px; display: flex; place-items: center; justify-content: center; } .code-preview__button--selected { font-weight: 700; color: var(--sl-color-primary-600); } .code-preview__button--codepen { display: flex; place-items: center; width: 6rem; } .code-preview__button:first-of-type { border-bottom-left-radius: 3px; } .code-preview__button:last-of-type { border-bottom-right-radius: 3px; } .code-preview__button:hover, .code-preview__button:active { box-shadow: 0 0 0 1px var(--sl-color-primary-400); border-right-color: transparent; background-color: var(--sl-color-primary-50); color: var(--sl-color-primary-600); z-index: 1; } .code-preview__button:focus-visible { outline: none; outline: var(--sl-focus-ring); z-index: 2; } .code-preview__toggle { position: relative; display: flex; flex: 1 1 auto; align-items: center; justify-content: center; width: 100%; color: var(--sl-color-neutral-600); cursor: pointer; } .code-preview__toggle svg { width: 1em; height: 1em; margin-left: 0.25rem; } .code-preview--expanded .code-preview__toggle svg { transform: rotate(180deg); } /* We can apply data-flavor="html|react" to any element on the page to toggle it when the flavor changes */ .flavor-html [data-flavor]:not([data-flavor='html']) { display: none; } .flavor-react [data-flavor]:not([data-flavor='react']) { display: none; } ================================================ FILE: docs/assets/styles/docs.css ================================================ :root { --docs-background-color: var(--sl-color-neutral-0); --docs-border-color: var(--sl-color-neutral-200); --docs-border-width: 1px; --docs-border-radius: var(--sl-border-radius-medium); --docs-content-max-width: 860px; --docs-sidebar-width: 320px; --docs-sidebar-transition-speed: 250ms; --docs-content-toc-max-width: 260px; --docs-content-padding: 2rem; --docs-content-vertical-spacing: 2rem; --docs-search-overlay-background: rgb(0 0 0 / 0.2); --docs-skip-to-main-width: 200px; } /* Light theme */ :root { color-scheme: normal; --sl-color-primary-50: var(--sl-color-sky-50); --sl-color-primary-100: var(--sl-color-sky-100); --sl-color-primary-200: var(--sl-color-sky-200); --sl-color-primary-300: var(--sl-color-sky-300); --sl-color-primary-400: var(--sl-color-sky-400); --sl-color-primary-500: var(--sl-color-sky-500); --sl-color-primary-600: var(--sl-color-sky-600); --sl-color-primary-700: var(--sl-color-sky-700); --sl-color-primary-800: var(--sl-color-sky-800); --sl-color-primary-900: var(--sl-color-sky-900); --sl-color-primary-950: var(--sl-color-sky-950); --docs-overlay-color: hsl(240 3.8% 46.1% / 33%); --docs-shadow-x-small: 0 1px 2px hsl(240 3.8% 46.1% / 12%); --docs-shadow-small: 0 1px 2px hsl(240 3.8% 46.1% / 24%); --docs-shadow-medium: 0 2px 4px hsl(240 3.8% 46.1% / 24%); --docs-shadow-large: 0 2px 8px hsl(240 3.8% 46.1% / 24%); --docs-shadow-x-large: 0 4px 16px hsl(240 3.8% 46.1% / 24%); } /* Dark theme */ .sl-theme-dark { color-scheme: dark; --docs-overlay-color: hsl(0 0% 0% / 66%); --docs-shadow-x-small: 0 1px 2px rgb(0 0 0 / 36%); --docs-shadow-small: 0 1px 2px rgb(0 0 0 / 48%); --docs-shadow-medium: 0 2px 4px rgb(0 0 0 / 48%); --docs-shadow-large: 0 2px 8px rgb(0 0 0 / 48%); --docs-shadow-x-large: 0 4px 16px rgb(0 0 0 / 48%); } /* Utils */ html.sl-theme-dark .only-light, html:not(.sl-theme-dark) .only-dark { display: none !important; } .visually-hidden:not(:focus-within) { position: absolute !important; width: 1px !important; height: 1px !important; clip: rect(0 0 0 0) !important; clip-path: inset(50%) !important; border: none !important; overflow: hidden !important; white-space: nowrap !important; padding: 0 !important; } .nowrap { white-space: nowrap; } @media screen and (max-width: 900px) { :root { --docs-content-padding: 1rem; } } /* Bare styles */ *, *:before, *:after { box-sizing: inherit; } a:focus, button:focus { outline: none; } a:focus-visible, button:focus-visible { outline: var(--sl-focus-ring); outline-offset: var(--sl-focus-ring-offset); } ::selection { background-color: var(--sl-color-primary-200); color: var(--sl-color-neutral-900); } /* Show custom elements only after they're registered */ :not(:defined), :not(:defined) * { opacity: 0; } :defined { opacity: 1; transition: 0.1s opacity; } html { height: 100%; box-sizing: border-box; line-height: var(--sl-line-height-normal); padding: 0; margin: 0; } body { height: 100%; font: 16px var(--sl-font-sans); font-weight: var(--sl-font-weight-normal); background-color: var(--docs-background-color); line-height: var(--sl-line-height-normal); color: var(--sl-color-neutral-900); padding: 0; margin: 0; overflow-x: hidden; -moz-osx-font-smoothing: grayscale; -webkit-font-smoothing: antialiased; } /* Common elements */ h1, h2, h3, h4, h5, h6 { font-family: var(--sl-font-sans); font-weight: var(--sl-font-weight-semibold); margin: 3rem 0 1.5rem 0; } h1:first-of-type { margin-top: 1rem; } h1 { font-size: 2.5rem; } h2 { font-size: 1.875rem; border-bottom: solid var(--docs-border-width) var(--docs-border-color); } h3 { font-size: 1.625rem; } h4 { font-size: 1.375rem; } h5 { font-size: 1.125rem; } h6 { font-size: 0.875rem; } p { margin-block-end: 1.5em; } img { max-width: 100%; } .badges img { border-radius: var(--sl-border-radius-medium); } .callout img, details img { width: 100%; margin-left: 0; margin-right: 0; } details pre { border: solid var(--docs-border-width) var(--docs-border-color); } a { color: var(--sl-color-primary-700); } a:hover { color: var(--sl-color-primary-800); } abbr[title] { text-decoration: none; border-bottom: dashed 1px var(--sl-color-primary-700); cursor: help; } em { font-style: italic; } strong { font-weight: var(--sl-font-weight-bold); } code { font-family: var(--sl-font-mono); font-size: 0.9125em; background-color: rgba(0 0 0 / 0.025); background-blend-mode: darken; border-radius: var(--docs-border-radius); padding: 0.125em 0.25em; } .sl-theme-dark code { background-color: rgba(255 255 255 / 0.03); } kbd { background: var(--sl-color-neutral-100); border: solid 1px var(--sl-color-neutral-200); box-shadow: inset 0 1px 0 0 var(--sl-color-neutral-0), inset 0 -1px 0 0 var(--sl-color-neutral-200); font-family: var(--sl-font-mono); font-size: 0.9125em; border-radius: var(--docs-border-radius); color: var(--sl-color-neutral-800); padding: 0.125em 0.4em; } ins { background-color: var(--sl-color-green-200); color: var(--sl-color-green-900); border-radius: var(--docs-border-radius); text-decoration: none; padding: 0.125em 0.25em; } s { background-color: var(--sl-color-red-200); color: var(--sl-color-red-900); border-radius: var(--docs-border-radius); text-decoration: none; padding: 0.125em 0.25em; } mark { background-color: var(--sl-color-yellow-200); border-radius: var(--docs-border-radius); padding: 0.125em 0.25em; } hr { border: none; border-bottom: solid var(--docs-border-width) var(--docs-border-color); margin: calc(var(--docs-content-vertical-spacing) * 2) 0; } /* Block quotes */ blockquote { position: relative; font-family: var(--sl-font-serif); font-size: 1.33rem; font-style: italic; color: var(--sl-color-neutral-700); background-color: var(--sl-color-neutral-100); border-radius: var(--docs-border-radius); border-left: solid 6px var(--sl-color-neutral-300); padding: 1.5rem; margin: 0 0 1.5rem 0; } blockquote > :first-child { margin-top: 0; } blockquote > :last-child { margin-bottom: 0; } /* Lists */ ul, ol { padding: 0; margin: 0 0 var(--docs-content-vertical-spacing) 2rem; } ul { list-style: disc; } li { padding: 0; margin: 0 0 0.25rem 0; } li ul, li ol { margin-top: 0.25rem; } ul ul:last-child, ul ol:last-child, ol ul:last-child, ol ol:last-child { margin-bottom: 0; } /* Anchor headings */ .anchor-heading { position: relative; color: inherit; text-decoration: none; } .anchor-heading a { text-decoration: none; color: inherit; } .anchor-heading a::after { content: '#'; color: var(--sl-color-primary-700); margin-inline: 0.5rem; opacity: 0; transition: 100ms opacity; } .anchor-heading:hover a::after, .anchor-heading:focus-within a::after { opacity: 1; } /* External links */ .external-link__icon { width: 0.75em; height: 0.75em; vertical-align: 0; margin-left: 0.25em; margin-right: 0.125em; } /* Tables */ table { max-width: 100%; border: none; border-collapse: collapse; color: inherit; margin-bottom: var(--docs-content-vertical-spacing); } table tr { border-bottom: solid var(--docs-border-width) var(--docs-border-color); } table th { font-weight: var(--sl-font-weight-semibold); text-align: left; } table td, table th { line-height: var(--sl-line-height-normal); padding: 1rem 1rem; } table th p:first-child, table td p:first-child { margin-top: 0; } table th p:last-child, table td p:last-child { margin-bottom: 0; } .table-scroll { max-width: 100%; overflow-x: auto; } .table-scroll code { white-space: nowrap; } th.table-name, th.table-event-detail { min-width: 15ch; } th.table-description { min-width: 50ch !important; max-width: 70ch; } /* Code blocks */ pre { position: relative; background-color: var(--sl-color-neutral-50); font-family: var(--sl-font-mono); color: var(--sl-color-neutral-900); border-radius: var(--docs-border-radius); padding: 1rem; white-space: pre; } .sl-theme-dark pre { background-color: var(--sl-color-neutral-50); } pre:not(:last-child) { margin-bottom: 1.5rem; } pre > code { display: block; background: none !important; border-radius: 0; hyphens: none; tab-size: 2; white-space: pre; padding: 1rem; margin: -1rem; overflow: auto; } pre .token.comment { color: var(--sl-color-neutral-600); } pre .token.prolog, pre .token.doctype, pre .token.cdata, pre .token.operator, pre .token.punctuation { color: var(--sl-color-neutral-700); } .namespace { opacity: 0.7; } pre .token.property, pre .token.keyword, pre .token.tag, pre .token.url { color: var(--sl-color-blue-700); } pre .token.symbol, pre .token.deleted { color: var(--sl-color-red-700); } pre .token.boolean, pre .token.constant, pre .token.selector, pre .token.attr-name, pre .token.string, pre .token.char, pre .token.builtin, pre .token.inserted { color: var(--sl-color-emerald-700); } pre .token.atrule, pre .token.attr-value, pre .token.number, pre .token.variable { color: var(--sl-color-violet-700); } pre .token.function, pre .token.class-name, pre .token.regex { color: var(--sl-color-orange-700); } pre .token.important { color: var(--sl-color-red-700); } pre .token.important, pre .token.bold { font-weight: bold; } pre .token.italic { font-style: italic; } /* Copy code button */ .copy-code-button { position: absolute; top: 0; right: 0; white-space: normal; color: var(--sl-color-neutral-800); transition: 150ms opacity, 150ms scale; } .copy-code-button::part(button) { background-color: var(--sl-color-neutral-50); border-radius: 0 var(--docs-border-radius) 0 var(--docs-border-radius); padding: 0.75rem; } .copy-code-button::part(button):hover { background-color: color-mix(in oklch, var(--sl-color-neutral-50), var(--sl-color-neutral-1000) 3%); } .copy-code-button::part(button):active { background-color: color-mix(in oklch, var(--sl-color-neutral-50), var(--sl-color-neutral-1000) 6%); } pre .copy-code-button { opacity: 0; scale: 0.75; } pre:hover .copy-code-button, .copy-code-button:focus-within { opacity: 1; scale: 1; } /* Callouts */ .callout { position: relative; background-color: var(--sl-color-neutral-100); border-left: solid 4px var(--sl-color-neutral-500); border-radius: var(--docs-border-radius); color: var(--sl-color-neutral-800); padding: 1.5rem 1.5rem 1.5rem 2rem; margin-bottom: var(--docs-content-vertical-spacing); } .callout > :first-child { margin-top: 0; } .callout > :last-child { margin-bottom: 0; } .callout--tip { background-color: var(--sl-color-primary-100); border-left-color: var(--sl-color-primary-600); color: var(--sl-color-primary-800); } .callout::before { content: ''; position: absolute; top: calc(50% - 0.8rem); left: calc(-0.8rem - 2px); width: 1.6rem; height: 1.6rem; display: flex; align-items: center; justify-content: center; font-family: var(--sl-font-serif); font-weight: var(--sl-font-weight-bold); color: var(--sl-color-neutral-0); clip-path: circle(50% at 50% 50%); } .callout--tip::before { content: 'i'; font-style: italic; background-color: var(--sl-color-primary-600); } .callout--warning { background-color: var(--sl-color-warning-100); border-left-color: var(--sl-color-warning-600); color: var(--sl-color-warning-800); } .callout--warning::before { content: '!'; background-color: var(--sl-color-warning-600); } .callout--danger { background-color: var(--sl-color-danger-100); border-left-color: var(--sl-color-danger-600); color: var(--sl-color-danger-800); } .callout--danger::before { content: '‼'; background-color: var(--sl-color-danger-600); } .callout + .callout { margin-top: calc(-0.5 * var(--docs-content-vertical-spacing)); } .callout a { color: inherit; } /* Aside */ .content aside { float: right; min-width: 300px; max-width: 50%; background: var(--sl-color-neutral-100); border-radius: var(--docs-border-radius); padding: 1rem; margin-left: 1rem; } .content aside > :first-child { margin-top: 0; } .content aside > :last-child { margin-bottom: 0; } @media screen and (max-width: 600px) { .content aside { float: none; width: calc(100% + (var(--docs-content-padding) * 2)); max-width: none; margin: var(--docs-content-vertical-spacing) calc(-1 * var(--docs-content-padding)); } } /* Details */ .content details { background-color: var(--sl-color-neutral-100); border-radius: var(--docs-border-radius); padding: 1rem; margin: 0 0 var(--docs-content-vertical-spacing) 0; } .content details summary { font-weight: var(--sl-font-weight-semibold); border-radius: var(--docs-border-radius); padding: 1rem; margin: -1rem; cursor: pointer; user-select: none; -webkit-user-select: none; } .content details summary span { padding-left: 0.5rem; } .content details[open] summary { border-bottom-left-radius: 0; border-bottom-right-radius: 0; margin-bottom: 1rem; } .content details[open] summary:focus-visible { border-radius: var(--docs-border-radius); } .content details > :last-child { margin-bottom: 0; } .content details > :nth-child(2) { margin-top: 0; } .content details + details { margin-top: calc(-1 * var(--docs-content-vertical-spacing) + (2 * var(--docs-border-width))); } /* Sidebar */ #sidebar { position: fixed; flex: 0; top: 0; left: 0; bottom: 0; z-index: 20; width: var(--docs-sidebar-width); background-color: var(--docs-background-color); border-right: solid var(--docs-border-width) var(--docs-border-color); border-radius: 0; padding: 2rem; margin: 0; overflow: auto; scrollbar-width: thin; transition: var(--docs-sidebar-transition-speed) translate ease-in-out; } #sidebar::-webkit-scrollbar { width: 4px; } #sidebar::-webkit-scrollbar-thumb { background: transparent; border-radius: 9999px; } #sidebar:hover::-webkit-scrollbar-thumb { background: var(--sl-color-neutral-400); } #sidebar:hover::-webkit-scrollbar-track { background: var(--sl-color-neutral-100); } #sidebar > header { margin-bottom: 1.5rem; } #sidebar > header h1 { margin: 0; } #sidebar > header a { display: block; } #sidebar nav a { text-decoration: none; } #sidebar nav h2 { font-size: var(--sl-font-size-medium); font-weight: var(--sl-font-weight-semibold); border-bottom: solid var(--docs-border-width) var(--docs-border-color); margin: 1.5rem 0 0.5rem 0; } #sidebar ul { padding: 0; margin: 0; } #sidebar ul li { list-style: none; padding: 0; margin: 0.125rem 0.5rem; } #sidebar ul ul ul { margin-left: 0.75rem; } #sidebar ul li a { line-height: 1.33; color: inherit; display: inline-block; padding: 0; } #sidebar ul li a:not(.active-link):hover { color: var(--sl-color-primary-700); } #sidebar nav .active-link { color: var(--sl-color-primary-700); border-bottom: dashed 1px var(--sl-color-primary-700); } #sidebar > header img { display: block; width: 100%; height: auto; margin: 0 auto; } @media screen and (max-width: 900px) { #sidebar { translate: -100%; } .sidebar-open #sidebar { translate: 0; } } .sidebar-version { font-size: var(--sl-font-size-x-small); color: var(--sl-color-neutral-500); text-align: right; margin-top: -0.5rem; margin-right: 1rem; margin-bottom: -0.5rem; } .sidebar-buttons { display: flex; justify-content: space-between; } /* Main content */ main { position: relative; padding: var(--docs-content-vertical-spacing) var(--docs-content-padding) calc(var(--docs-content-vertical-spacing) * 2) var(--docs-content-padding); margin-left: var(--docs-sidebar-width); } .sidebar-open .content { margin-left: 0; } .content__body > :last-child { margin-bottom: 0; } @media screen and (max-width: 900px) { main { margin-left: 0; } } /* Component layouts */ .content { display: grid; grid-template-columns: 100%; gap: 2rem; position: relative; max-width: var(--docs-content-max-width); margin: 0 auto; } .content--with-toc { /* There's a 2rem gap, so we need to remove it from the column */ grid-template-columns: calc(75% - 2rem) min(25%, var(--docs-content-toc-max-width)); max-width: calc(var(--docs-content-max-width) + var(--docs-content-toc-max-width)); } .content__body { order: 1; width: 100%; } .content:not(.content--with-toc) .content__toc { display: none; } .content__toc { order: 2; display: flex; flex-direction: column; margin-top: 0; } .content__toc ul { position: sticky; top: 5rem; max-height: calc(100vh - 6rem); font-size: var(--sl-font-size-small); line-height: 1.33; border-left: solid 1px var(--sl-color-neutral-200); list-style: none; padding: 1rem 0; margin: 0; padding-left: 1rem; overflow-y: auto; } .content__toc li { padding: 0 0 0 0.5rem; margin: 0; } .content__toc li[data-level='3'] { margin-left: 1rem; } /* We don't use them, but just in case */ .content__toc li[data-level='4'], .content__toc li[data-level='5'], .content__toc li[data-level='6'] { margin-left: 2rem; } .content__toc li:not(:last-of-type) { margin-bottom: 0.6rem; } .content__toc a { color: var(--sl-color-neutral-700); text-decoration: none; } .content__toc a:hover { color: var(--sl-color-primary-700); } .content__toc a.active { color: var(--sl-color-primary-700); border-bottom: dashed 1px var(--sl-color-primary-700); } .content__toc .top a { font-weight: var(--sl-font-weight-semibold); color: var(--sl-color-neutral-500); } @media screen and (max-width: 1024px) { .content { grid-template-columns: 100%; gap: 0; } .content__toc { position: relative; order: 1; } .content__toc ul { display: flex; justify-content: start; gap: 1rem 1.5rem; position: static; border: none; border-bottom: solid 1px var(--sl-color-neutral-200); border-radius: 0; padding: 1rem 1.5rem 1rem 0.5rem; /* extra right padding to hide the fade effect */ margin-top: 1rem; overflow-x: auto; } .content__toc ul::after { content: ''; position: absolute; top: 0; bottom: 1rem; /* don't cover the scrollbar */ right: 0; width: 2rem; background: linear-gradient(90deg, rgba(0 0 0 / 0) 0%, var(--sl-color-neutral-0) 100%); } .content__toc li { white-space: nowrap; } .content__toc li:not(:last-of-type) { margin-bottom: 0; } .content__toc [data-level]:not([data-level='2']) { display: none; } .content__body { order: 2; } } /* Menu toggle */ #menu-toggle { display: none; position: fixed; z-index: 30; top: 0.25rem; left: 0.25rem; height: auto; width: auto; color: black; border: none; border-radius: 50%; background: var(--sl-color-neutral-0); padding: 0.5rem; margin: 0; cursor: pointer; transition: 250ms scale ease, 250ms rotate ease; } @media screen and (max-width: 900px) { #menu-toggle { display: flex; } } .sl-theme-dark #menu-toggle { color: white; } #menu-toggle:hover { scale: 1.1; } #menu-toggle svg { width: 1.25rem; height: 1.25rem; } html.sidebar-open #menu-toggle { rotate: 180deg; } /* Skip to main content */ #skip-to-main { position: fixed; top: 0.25rem; left: calc(50% - var(--docs-skip-to-main-width) / 2); z-index: 100; width: var(--docs-skip-to-main-width); text-align: center; text-decoration: none; border-radius: 9999px; background: var(--sl-color-neutral-0); color: var(--sl-color-neutral-1000); padding: 0.5rem; } /* Icon toolbar */ #icon-toolbar { display: flex; position: fixed; top: 0; right: 1rem; z-index: 10; background: var(--sl-color-neutral-800); border-bottom-left-radius: calc(var(--docs-border-radius) * 2); border-bottom-right-radius: calc(var(--docs-border-radius) * 2); padding: 0.125rem 0.25rem; } #icon-toolbar button, #icon-toolbar a { flex: 0 0 auto; display: inline-flex; align-items: center; width: auto; height: auto; background: transparent; border: none; border-radius: var(--docs-border-radius); font-size: 1.25rem; color: var(--sl-color-neutral-0); text-decoration: none; padding: 0.5rem; margin: 0; cursor: pointer; } #theme-selector:not(:defined) { /* Hide when not defined to prevent extra wide icon toolbar while loading */ display: none; } #theme-selector sl-menu { /* Set an initial size to prevent width being too small when first opening on small screen width */ width: 140px; } #theme-selector sl-button { transition: 250ms scale ease; } #theme-selector sl-button::part(base) { color: var(--sl-color-neutral-0); } #theme-selector sl-button::part(label) { display: flex; padding: 0.5rem; } #theme-selector sl-icon { font-size: 1.25rem; } .sl-theme-dark #theme-selector sl-button::part(base) { color: var(--sl-color-neutral-1000); } .sl-theme-dark #icon-toolbar { background: var(--sl-color-neutral-200); } .sl-theme-dark #icon-toolbar button, .sl-theme-dark #icon-toolbar a { color: var(--sl-color-neutral-1000); } #icon-toolbar a:not(:last-child), #icon-toolbar button:not(:last-child) { margin-right: 0.25rem; } @media screen and (max-width: 900px) { #icon-toolbar { right: 0; border-bottom-right-radius: 0; } #icon-toolbar button, #icon-toolbar a { font-size: 1rem; padding: 0.5rem; } #theme-selector sl-icon { font-size: 1rem; } } /* Sidebar addons */ #sidebar-addons:not(:empty) { margin-bottom: var(--docs-content-vertical-spacing); } /* Print styles */ @media print { a:not(.anchor-heading)[href]::after { content: ' (' attr(href) ')'; } details, pre, .callout { border: solid var(--docs-border-width) var(--docs-border-color); } details summary { list-style: none; } details summary span { padding-left: 0; } details summary::marker, details summary::-webkit-details-marker { display: none; } .callout::before { border: solid var(--docs-border-width) var(--docs-border-color); } .component-page__navigation, .copy-code-button, .code-preview__buttons, .code-preview__resizer { display: none !important; } .flavor-html .code-preview__source--html, .flavor-react .code-preview__source--react { display: block !important; } .flavor-html .code-preview__source--html > pre, .flavor-react .code-preview__source--react > pre { border: none; } .code-preview__source-group { border-bottom: solid 1px var(--sl-color-neutral-200); border-bottom-left-radius: var(--sl-border-radius-medium); border-bottom-right-radius: var(--sl-border-radius-medium); } #sidebar { display: none; } #content { margin-left: 0; } #menu-toggle, #icon-toolbar, .external-link__icon { display: none; } } /* Splash */ .splash { display: flex; padding-top: 2rem; } .splash-start { min-width: 420px; } .splash-start h1 { font-size: var(--sl-font-size-large); font-weight: var(--sl-font-weight-normal); } .splash li img { width: 1em; height: 1em; vertical-align: -2px; } .splash-end { display: flex; align-items: flex-end; width: auto; padding-left: 1rem; } .splash-image { width: 100%; height: auto; } .splash-logo { max-width: 22rem; } .splash-start h1:first-of-type { font-size: var(--sl-font-size-large); margin: 0 0 0.5rem 0; } @media screen and (max-width: 1280px) { .splash { display: block; } .splash-start { min-width: 0; padding-bottom: 1rem; } .splash-end { padding: 0; } .splash-image { display: block; max-width: 400px; } /* Shields */ .splash + p { margin-top: 2rem; } } /* Component headers */ .component-header h1 { margin-bottom: 0; } .component-header__tag { margin-top: -0.5rem; margin-bottom: 0.5rem; } .component-header__tag code { background: none; color: var(--sl-color-neutral-600); font-size: var(--sl-font-size-large); padding: 0; margin: 0; } .component-header__info { margin-bottom: var(--sl-spacing-x-large); } .component-summary { font-size: var(--sl-font-size-large); line-height: 1.6; margin: 2rem 0; } /* Repo buttons */ .sidebar-buttons { display: flex; gap: 0.125rem; justify-content: space-between; } .sidebar-buttons .repo-button { flex: 1 1 auto; } .repo-button--github sl-icon { color: var(--sl-color-neutral-700); } .repo-button--star sl-icon { color: var(--sl-color-yellow-500); } .repo-button--twitter sl-icon { color: var(--sl-color-sky-500); } @media screen and (max-width: 400px) { :not(.sidebar-buttons) > .repo-button { width: 100%; margin-bottom: 1rem; } } body[data-page^='/tokens/'] .table-wrapper td:first-child, body[data-page^='/tokens/'] .table-wrapper td:first-child code { white-space: nowrap; } /* Border radius demo */ .border-radius-demo { width: 3rem; height: 3rem; background: var(--sl-color-primary-600); } /* Transition demo */ .transition-demo { position: relative; background: var(--sl-color-neutral-200); width: 8rem; height: 2rem; } .transition-demo:after { content: ''; position: absolute; background-color: var(--sl-color-primary-600); top: 0; left: 0; width: 0; height: 100%; transition-duration: inherit; transition-property: width; } .transition-demo:hover:after { width: 100%; } /* Spacing demo */ .spacing-demo { width: 100px; background: var(--sl-color-primary-600); } /* Elevation demo */ .elevation-demo { background: transparent; border-radius: 3px; width: 4rem; height: 4rem; margin: 1rem; } /* Color palettes */ .color-palette { display: grid; grid-template-columns: 200px repeat(11, 1fr); gap: 1rem var(--sl-spacing-2x-small); margin: 2rem 0; } .color-palette__name { font-size: var(--sl-font-size-medium); font-weight: var(--sl-font-weight-semibold); grid-template-columns: repeat(11, 1fr); } .color-palette__name code { background: none; font-size: var(--sl-font-size-x-small); } .color-palette__example { font-size: var(--sl-font-size-x-small); text-align: center; } .color-palette__swatch { height: 3rem; border-radius: var(--sl-border-radius-small); } .color-palette__swatch--border { box-shadow: inset 0 0 0 1px var(--sl-color-neutral-300); } @media screen and (max-width: 1200px) { .color-palette { grid-template-columns: repeat(6, 1fr); } .color-palette__name { grid-column-start: span 6; } } /* custom styling - WA dialog */ #wa-dialog { --action-color: #cd491c; --action-color-hover: color-mix(in oklab, var(--action-color), var(--sl-color-neutral-1000) 5%); } #wa-dialog a { color: var(--action-color); } #wa-dialog a:hover { color: var(--action-color-hover); } #wa-dialog::part(header) { display: none; } #wa-dialog::part(body) { padding: 0; } #wa-dialog::part(footer) { background-color: var(--sl-color-gray-50); font-size: var(--sl-font-size-small); text-align: start; } #wa-dialog::part(panel) { overflow: clip; /* preserve border-radius of panel w/ footer + billboard backgrounds */ } #wa-dialog .dialog-body { padding: var(--sl-spacing-x-large) var(--body-spacing); } #wa-dialog .dialog-billboard { position: relative; padding: var(--sl-spacing-3x-large) var(--sl-spacing-2x-large); gap: var(--sl-spacing-medium); background: linear-gradient(to bottom, #f46a45, #cd491c); color: var(--sl-color-neutral-0); } #wa-dialog .dialog-billboard::after { content: ''; position: absolute; inset: 0; z-index: 0; background-color: transparent; background-image: url('/assets/images/wa-bg-pattern.svg'); background-repeat: repeat; opacity: 0.3; } #wa-dialog .dialog-billboard-content { position: relative; z-index: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sl-spacing-x-small); } #wa-dialog .dialog-billboard-content .wa-logo { inline-size: auto; max-inline-size: 100%; block-size: 4rem; } #wa-dialog .dialog-billboard-content h2 { margin-block: 0; border-bottom: none; text-align: center; line-height: 1.4; font-weight: var(--sl-font-weight-bold); font-size: var(--sl-font-size-x-large); } #wa-dialog p { margin-block-end: var(--sl-spacing-small); } #wa-dialog p:last-of-type { margin-block-end: 0; } #wa-dialog h4, #wa-dialog p { margin-block-start: 0; } #wa-dialog .dialog-actions { display: flex; flex-wrap: wrap; gap: var(--sl-spacing-x-small); justify-content: space-between; align-items: center; } #wa-dialog .dialog-actions .wa-ctas { display: flex; flex-wrap: wrap; gap: var(--sl-spacing-x-small); align-items: center; } #wa-dialog sl-divider { --spacing: var(--sl-spacing-x-large); } #wa-dialog .wa-cta::part(base) { border-color: var(--action-color); } #wa-dialog .close-wa-dialog::part(label) { color: var(--action-color); } #wa-dialog .close-wa-dialog::part(label):hover { color: var(--action-color-hover); } #wa-dialog .close-wa-cta::part(label):hover { color: var(--sl-color-neutral-0); } #wa-dialog .wa-cta::part(base) { color: var(--action-color); } #wa-dialog .wa-pro-cta::part(base) { background-color: var(--action-color); border-color: var(--action-color); } #wa-dialog .wa-cta::part(base):hover, #wa-dialog .wa-pro-cta::part(base):hover { background-color: var(--action-color-hover); border-color: var(--action-color-hover); color: var(--sl-color-neutral-0); } ================================================ FILE: docs/assets/styles/search.css ================================================ /* Search plugin */ :root, :root.sl-theme-dark { --docs-search-box-background: var(--sl-color-neutral-0); --docs-search-box-border-width: 1px; --docs-search-box-border-color: var(--sl-color-neutral-300); --docs-search-box-color: var(--sl-color-neutral-600); --docs-search-dialog-background: var(--sl-color-neutral-0); --docs-search-border-width: var(--docs-border-width); --docs-search-border-color: var(--docs-border-color); --docs-search-text-color: var(--sl-color-neutral-900); --docs-search-text-color-muted: var(--sl-color-neutral-500); --docs-search-font-weight-normal: var(--sl-font-weight-normal); --docs-search-font-weight-semibold: var(--sl-font-weight-semibold); --docs-search-border-radius: calc(2 * var(--docs-border-radius)); --docs-search-accent-color: var(--sl-color-primary-600); --docs-search-icon-color: var(--sl-color-neutral-500); --docs-search-icon-color-active: var(--sl-color-neutral-600); --docs-search-shadow: var(--docs-shadow-x-large); --docs-search-result-background-hover: var(--sl-color-neutral-100); --docs-search-result-color-hover: var(--sl-color-neutral-900); --docs-search-result-background-active: var(--sl-color-primary-600); --docs-search-result-color-active: var(--sl-color-neutral-0); --docs-search-focus-ring: var(--sl-focus-ring); --docs-search-overlay-background: rgb(0 0 0 / 0.33); } :root.sl-theme-dark { --docs-search-overlay-background: rgb(71 71 71 / 0.33); } body.search-visible { padding-right: var(--docs-search-scroll-lock-size) !important; overflow: hidden !important; } /* Search box */ .search-box { flex: 1 1 auto; display: flex; align-items: center; width: 100%; border: none; border-radius: 9999px; background: var(--docs-search-box-background); border: solid var(--docs-search-box-border-width) var(--docs-search-box-border-color); font: inherit; color: var(--docs-search-box-color); padding: 0.75rem 1rem; margin: var(--sl-spacing-large) 0; cursor: pointer; } .search-box span { flex: 1 1 auto; width: 1rem; height: 1rem; text-align: left; line-height: 1; margin: 0 0.75rem; } .search-box:focus { outline: none; } .search-box:focus-visible { outline: var(--docs-search-focus-ring); } /* Site search */ .search { position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 9999; } .search[hidden] { display: none; } .search__overlay { position: absolute; top: 0; left: 0; right: 0; bottom: 0; background: var(--docs-search-overlay-background); z-index: -1; } .search__dialog { width: 100%; height: 100%; max-width: none; max-height: none; background: transparent; border: none; padding: 0; margin: 0; } .search__dialog:focus { outline: none; } .search__dialog::backdrop { display: none; } /* Fixes an iOS Safari 16.4 bug that draws the parent element's border radius incorrectly when showing/hiding results */ .search__header { background-color: var(--docs-search-dialog-background); border-radius: var(--docs-search-border-radius); } .search--has-results .search__header { border-top-left-radius: var(--docs-search-border-radius); border-top-right-radius: var(--docs-search-border-radius); border-bottom-left-radius: 0; border-bottom-right-radius: 0; } .search__content { display: flex; flex-direction: column; width: 100%; max-width: 500px; max-height: calc(100vh - 20rem); background-color: var(--docs-search-dialog-background); border-radius: var(--docs-search-border-radius); box-shadow: var(--docs-search-shadow); padding: 0; margin: 10rem auto; } @media screen and (max-width: 900px) { .search__content { max-width: calc(100% - 2rem); max-height: calc(90svh); margin: 4vh 1rem; } } .search__input-wrapper { display: flex; align-items: center; } .search__input-wrapper sl-icon { width: 1.5rem; height: 1.5rem; flex: 0 0 auto; color: var(--docs-search-icon-color); margin: 0 1.5rem; } .search__clear-button { display: flex; background: none; border: none; font: inherit; padding: 0; margin: 0; cursor: pointer; } .search__clear-button[hidden] { display: none; } .search__clear-button:active sl-icon { color: var(--docs-search-icon-color-active); } .search__input { flex: 1 1 auto; min-width: 0; border: none; font: inherit; font-size: 1.5rem; font-weight: var(--docs-search-font-weight-normal); color: var(--docs-search-text-color); background: transparent; padding: 1rem 0; margin: 0; } .search__input::placeholder { color: var(--docs-search-text-color-muted); } .search__input::-webkit-search-decoration, .search__input::-webkit-search-cancel-button, .search__input::-webkit-search-results-button, .search__input::-webkit-search-results-decoration { display: none; } .search__input:focus, .search__input:focus-visible { outline: none; } .search__body { flex: 1 1 auto; overflow: auto; } .search--has-results .search__body { border-top: solid var(--docs-search-border-width) var(--docs-search-border-color); } .search__results { display: none; line-height: 1.2; list-style: none; padding: 0.5rem 0; margin: 0; } .search--has-results .search__results { display: block; } .search__results a { display: block; text-decoration: none; padding: 0.5rem 1.5rem; } .search__results a:focus-visible { outline: var(--docs-search-focus-ring); } .search__results li a:hover, .search__results li a:hover small { background-color: var(--docs-search-result-background-hover); color: var(--docs-search-result-color-hover); } .search__results li[data-selected='true'] a, .search__results li[data-selected='true'] a * { outline: none; background-color: var(--docs-search-result-background-active); color: var(--docs-search-result-color-active); } .search__results h3 { font-weight: var(--docs-search-font-weight-semibold); margin: 0; } .search__results small { display: block; color: var(--docs-search-text-color-muted); } .search__result { padding: 0; margin: 0; } .search__result a { display: flex; align-items: center; gap: 1rem; } .search__result-icon { flex: 0 0 auto; display: flex; color: var(--docs-search-text-color-muted); } .search__result-icon sl-icon { font-size: 1.5rem; } .search__result__details { width: calc(100% - 3rem); } .search__result-title, .search__result-description, .search__result-url { max-width: 400px; line-height: 1.3; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .search__result-title { font-size: 1.2rem; font-weight: var(--docs-search-font-weight-semibold); color: var(--docs-search-accent-color); } .search__result-description { font-size: 0.875rem; color: var(--docs-search-text-color); } .search__result-url { font-size: 0.875rem; color: var(--docs-search-text-color-muted); } .search__empty { display: none; border-top: solid var(--docs-search-border-width) var(--docs-search-border-color); text-align: center; color: var(--docs-search-text-color-muted); padding: 2rem; } .search--no-results .search__empty { display: block; } .search__footer { display: flex; justify-content: center; gap: 2rem; border-top: solid var(--docs-search-border-width) var(--docs-search-border-color); border-bottom-left-radius: inherit; border-bottom-right-radius: inherit; padding: 1rem; } .search__footer small { color: var(--docs-search-text-color-muted); } .search__footer small kbd:last-of-type { margin-right: 0.25rem; } @media screen and (max-width: 900px) { .search__footer { display: none; } } ================================================ FILE: docs/eleventy.config.cjs ================================================ /* eslint-disable no-invalid-this */ const fs = require('fs'); const path = require('path'); const lunr = require('lunr'); const { capitalCase } = require('change-case'); const { JSDOM } = require('jsdom'); const { customElementsManifest, getAllComponents } = require('./_utilities/cem.cjs'); const shoelaceFlavoredMarkdown = require('./_utilities/markdown.cjs'); const activeLinks = require('./_utilities/active-links.cjs'); const anchorHeadings = require('./_utilities/anchor-headings.cjs'); const codePreviews = require('./_utilities/code-previews.cjs'); const copyCodeButtons = require('./_utilities/copy-code-buttons.cjs'); const externalLinks = require('./_utilities/external-links.cjs'); const highlightCodeBlocks = require('./_utilities/highlight-code.cjs'); const tableOfContents = require('./_utilities/table-of-contents.cjs'); const prettier = require('./_utilities/prettier.cjs'); const scrollingTables = require('./_utilities/scrolling-tables.cjs'); const typography = require('./_utilities/typography.cjs'); const replacer = require('./_utilities/replacer.cjs'); const assetsDir = 'assets'; const cdndir = 'cdn'; const npmdir = 'dist'; const allComponents = getAllComponents(); let hasBuiltSearchIndex = false; module.exports = function (eleventyConfig) { // // Global data // eleventyConfig.addGlobalData('baseUrl', 'https://shoelace.style/'); // the production URL eleventyConfig.addGlobalData('layout', 'default'); // make 'default' the default layout eleventyConfig.addGlobalData('toc', true); // enable the table of contents eleventyConfig.addGlobalData('meta', { title: 'Shoelace', description: 'A forward-thinking library of web components.', image: 'images/og-image.png', version: customElementsManifest.package.version, components: allComponents, cdndir, npmdir }); // // Layout aliases // eleventyConfig.addLayoutAlias('default', 'default.njk'); // // Copy assets // eleventyConfig.addPassthroughCopy(assetsDir); eleventyConfig.setServerPassthroughCopyBehavior('passthrough'); // emulates passthrough copy during --serve // // Functions // // Generates a URL relative to the site's root eleventyConfig.addNunjucksGlobal('rootUrl', (value = '', absolute = false) => { value = path.join('/', value); return absolute ? new URL(value, eleventyConfig.globalData.baseUrl).toString() : value; }); // Generates a URL relative to the site's asset directory eleventyConfig.addNunjucksGlobal('assetUrl', (value = '', absolute = false) => { value = path.join(`/${assetsDir}`, value); return absolute ? new URL(value, eleventyConfig.globalData.baseUrl).toString() : value; }); // Fetches a specific component's metadata eleventyConfig.addNunjucksGlobal('getComponent', tagName => { const component = allComponents.find(c => c.tagName === tagName); if (!component) { throw new Error( `Unable to find a component called "${tagName}". Make sure the file name is the same as the component's tag ` + `name (minus the sl- prefix).` ); } return component; }); // // Custom markdown syntaxes // eleventyConfig.setLibrary('md', shoelaceFlavoredMarkdown); // // Filters // eleventyConfig.addFilter('markdown', content => { return shoelaceFlavoredMarkdown.render(content); }); eleventyConfig.addFilter('markdownInline', content => { return shoelaceFlavoredMarkdown.renderInline(content); }); // Trims whitespace and pipes from the start and end of a string. Useful for CEM types, which can be pipe-delimited. // With Prettier 3, this means a leading pipe will exist if the line wraps. eleventyConfig.addFilter('trimPipes', content => { return typeof content === 'string' ? content.replace(/^(\s|\|)/g, '').replace(/(\s|\|)$/g, '') : content; }); eleventyConfig.addFilter('classNameToComponentName', className => { let name = capitalCase(className.replace(/^Sl/, '')); if (name === 'Qr Code') name = 'QR Code'; // manual override return name; }); eleventyConfig.addFilter('removeSlPrefix', tagName => { return tagName.replace(/^sl-/, ''); }); // // Transforms // eleventyConfig.addTransform('html-transform', function (rawContent) { let content = replacer(rawContent, [ { pattern: '%VERSION%', replacement: customElementsManifest.package.version }, { pattern: '%CDNDIR%', replacement: cdndir }, { pattern: '%NPMDIR%', replacement: npmdir } ]); // Parse the template and get a Document object const doc = new JSDOM(content, { // We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily // identify which ones are internal and which ones are external. url: `https://internal/` }).window.document; // DOM transforms activeLinks(doc, { pathname: this.page.url }); anchorHeadings(doc, { within: '#content .content__body', levels: ['h2', 'h3', 'h4', 'h5'] }); tableOfContents(doc, { levels: ['h2', 'h3'], container: '#content .content__toc > ul', within: '#content .content__body' }); codePreviews(doc); externalLinks(doc, { target: '_blank' }); highlightCodeBlocks(doc); scrollingTables(doc); copyCodeButtons(doc); // must be after codePreviews + highlightCodeBlocks typography(doc, '#content'); // Serialize the Document object to an HTML string and prepend the doctype content = `\n${doc.documentElement.outerHTML}`; // String transforms content = prettier(content); return content; }); // // Build a search index // eleventyConfig.on('eleventy.after', ({ results }) => { // We only want to build the search index on the first run so all pages get indexed. if (hasBuiltSearchIndex) { return; } const map = {}; const searchIndexFilename = path.join(eleventyConfig.dir.output, assetsDir, 'search.json'); const lunrInput = path.resolve('../node_modules/lunr/lunr.min.js'); const lunrOutput = path.join(eleventyConfig.dir.output, assetsDir, 'scripts/lunr.js'); const searchIndex = lunr(function () { // The search index uses these field names extensively, so shortening them can save some serious bytes. The // initial index file went from 468 KB => 401 KB by using single-character names! this.ref('id'); // id this.field('t', { boost: 50 }); // title this.field('h', { boost: 25 }); // headings this.field('c'); // content results.forEach((result, index) => { const url = path .join('/', path.relative(eleventyConfig.dir.output, result.outputPath)) .replace(/\\/g, '/') // convert backslashes to forward slashes .replace(/\/index.html$/, '/'); // convert trailing /index.html to / const doc = new JSDOM(result.content, { // We must set a default URL so links are parsed with a hostname. Let's use a bogus TLD so we can easily // identify which ones are internal and which ones are external. url: `https://internal/` }).window.document; const content = doc.querySelector('#content'); // Get title and headings const title = (doc.querySelector('title')?.textContent || path.basename(result.outputPath)).trim(); const headings = [...content.querySelectorAll('h1, h2, h3, h4')] .map(heading => heading.textContent) .join(' ') .replace(/\s+/g, ' ') .trim(); // Remove code blocks and whitespace from content [...content.querySelectorAll('code[class|=language]')].forEach(code => code.remove()); const textContent = content.textContent.replace(/\s+/g, ' ').trim(); // Update the index and map this.add({ id: index, t: title, h: headings, c: textContent }); map[index] = { title, url }; }); }); // Copy the Lunr search client and write the index fs.mkdirSync(path.dirname(lunrOutput), { recursive: true }); fs.copyFileSync(lunrInput, lunrOutput); fs.writeFileSync(searchIndexFilename, JSON.stringify({ searchIndex, map }), 'utf-8'); hasBuiltSearchIndex = true; }); // // Send a signal to stdout that let's the build know we've reached this point // eleventyConfig.on('eleventy.after', () => { console.log('[eleventy.after]'); }); // // Dev server options (see https://www.11ty.dev/docs/dev-server/#options) // eleventyConfig.setServerOptions({ domDiff: false, // disable dom diffing so custom elements don't break on reload, port: 4000, // if port 4000 is taken, 11ty will use the next one available watch: ['cdn/**/*'] // additional files to watch that will trigger server updates (array of paths or globs) }); // // 11ty config // return { dir: { input: 'pages', output: '../_site', includes: '../_includes' // resolved relative to the input dir }, markdownTemplateEngine: 'njk', // use Nunjucks instead of Liquid for markdown files templateEngineOverride: ['njk'] // just Nunjucks and then markdown }; }; ================================================ FILE: docs/pages/404.md ================================================ --- meta: title: Page Not Found description: "The page you were looking for couldn't be found." permalink: 404.html toc: false ---
      # Page Not Found ![A UFO takes one of the little worker monsters](/assets/images/undraw-taken.svg) The page you were looking for couldn't be found. Press [[/]] to search, or [head back to the homepage](/).
      ================================================ FILE: docs/pages/components/alert.md ================================================ --- meta: title: Alert description: Alerts are used to display important messages inline or as toast notifications. layout: component --- ```html:preview This is a standard alert. You can customize its content and even the icon. ``` ```jsx:react import SlAlert from '@shoelace-style/shoelace/dist/react/alert'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => ( This is a standard alert. You can customize its content and even the icon. ); ``` :::tip Alerts will not be visible if the `open` attribute is not present. ::: ## Examples ### Variants Set the `variant` attribute to change the alert's variant. ```html:preview This is super informative
      You can tell by how pretty the alert is.

      Your changes have been saved
      You can safely exit the app now.

      Your settings have been updated
      Settings will take effect on next login.

      Your session has ended
      Please login again to continue.

      Your account has been deleted
      We're very sorry to see you go!
      ``` ```jsx:react import SlAlert from '@shoelace-style/shoelace/dist/react/alert'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => ( <> This is super informative
      You can tell by how pretty the alert is.

      Your changes have been saved
      You can safely exit the app now.

      Your settings have been updated
      Settings will take effect on next login.

      Your session has ended
      Please login again to continue.

      Your account has been deleted
      We're very sorry to see you go!
      ); ``` ### Closable Add the `closable` attribute to show a close button that will hide the alert. ```html:preview You can close this alert any time! ``` ```jsx:react import { useState } from 'react'; import SlAlert from '@shoelace-style/shoelace/dist/react/alert'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => { const [open, setOpen] = useState(true); function handleHide() { setOpen(false); setTimeout(() => setOpen(true), 2000); } return ( You can close this alert any time! ); }; ``` ### Without Icons Icons are optional. Simply omit the `icon` slot if you don't want them. ```html:preview Nothing fancy here, just a simple alert. ``` ```jsx:react import SlAlert from '@shoelace-style/shoelace/dist/react/alert'; const App = () => ( Nothing fancy here, just a simple alert. ); ``` ### Duration Set the `duration` attribute to automatically hide an alert after a period of time. This is useful for alerts that don't require acknowledgement. ```html:preview
      Show Alert This alert will automatically hide itself after three seconds, unless you interact with it.
      ``` ```jsx:react import { useState } from 'react'; import SlAlert from '@shoelace-style/shoelace/dist/react/alert'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const css = ` .alert-duration sl-alert { margin-top: var(--sl-spacing-medium); } `; const App = () => { const [open, setOpen] = useState(false); return ( <>
      setOpen(true)}> Show Alert setOpen(false)}> This alert will automatically hide itself after three seconds, unless you interact with it.
      ); }; ``` ### Countdown Set the `countdown` attribute to display a loading bar that indicates the alert remaining time. This is useful for alerts with relatively long duration. ```html:preview
      Show Alert You're not stuck, the alert will close after a pretty long duration.
      ``` ```jsx:react import { useState } from 'react'; import SlAlert from '@shoelace-style/shoelace/dist/react/alert'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const css = ` .alert-countdown sl-alert { margin-top: var(--sl-spacing-medium); } `; const App = () => { const [open, setOpen] = useState(false); return ( <>
      setOpen(true)}> Show Alert setOpen(false)}> You're not stuck, the alert will close after a pretty long duration.
      ); }; ``` ### Toast Notifications To display an alert as a toast notification, or "toast", create the alert and call its `toast()` method. This will move the alert out of its position in the DOM and into [the toast stack](#the-toast-stack) where it will be shown. Once dismissed, it will be removed from the DOM completely. To reuse a toast, store a reference to it and call `toast()` again later on. You should always use the `closable` attribute so users can dismiss the notification. It's also common to set a reasonable `duration` when the notification doesn't require acknowledgement. ```html:preview
      Primary Success Neutral Warning Danger This is super informative
      You can tell by how pretty the alert is.
      Your changes have been saved
      You can safely exit the app now.
      Your settings have been updated
      Settings will take effect on next login.
      Your session has ended
      Please login again to continue.
      Your account has been deleted
      We're very sorry to see you go!
      ``` ```jsx:react import { useRef } from 'react'; import SlAlert from '@shoelace-style/shoelace/dist/react/alert'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; function showToast(alert) { alert.toast(); } const App = () => { const primary = useRef(null); const success = useRef(null); const neutral = useRef(null); const warning = useRef(null); const danger = useRef(null); return ( <> primary.current.toast()}> Primary success.current.toast()}> Success neutral.current.toast()}> Neutral warning.current.toast()}> Warning danger.current.toast()}> Danger This is super informative
      You can tell by how pretty the alert is.
      Your changes have been saved
      You can safely exit the app now.
      Your settings have been updated
      Settings will take effect on next login.
      Your session has ended
      Please login again to continue.
      Your account has been deleted
      We're very sorry to see you go!
      ); }; ``` ### Creating Toasts Imperatively For convenience, you can create a utility that emits toast notifications with a function call rather than composing them in your HTML. To do this, generate the alert with JavaScript, append it to the body, and call the `toast()` method as shown in the example below. ```html:preview
      Create Toast
      ``` ### The Toast Stack The toast stack is a fixed position singleton element created and managed internally by the alert component. It will be added and removed from the DOM as needed when toasts are shown. When more than one toast is visible, they will stack vertically in the toast stack. By default, the toast stack is positioned at the top-right of the viewport. You can change its position by targeting `.sl-toast-stack` in your stylesheet. To make toasts appear at the top-left of the viewport, for example, use the following styles. ```css .sl-toast-stack { left: 0; right: auto; } ``` :::tip By design, it is not possible to show toasts in more than one stack simultaneously. Such behavior is confusing and makes for a poor user experience. ::: ================================================ FILE: docs/pages/components/animated-image.md ================================================ --- meta: title: Animated Image description: A component for displaying animated GIFs and WEBPs that play and pause on interaction. layout: component --- ```html:preview ``` ```jsx:react import SlAnimatedImage from '@shoelace-style/shoelace/dist/react/animated-image'; const App = () => ( ); ``` :::tip This component uses `` to draw freeze frames, so images are subject to [cross-origin restrictions](https://developer.mozilla.org/en-US/docs/Web/HTML/CORS_enabled_image). ::: ## Examples ### WEBP Images Both GIF and WEBP images are supported. ```html:preview ``` ```jsx:react import SlAnimatedImage from '@shoelace-style/shoelace/dist/react/animated-image'; const App = () => ( ); ``` ### Setting a Width and Height To set a custom size, apply a width and/or height to the host element. ```html:preview ``` {% raw %} ```jsx:react import SlAnimatedImage from '@shoelace-style/shoelace/dist/react/animated-image'; const App = () => ( ); ``` {% endraw %} ### Customizing the Control Box You can change the appearance and location of the control box by targeting the `control-box` part in your styles. ```html:preview ``` ```jsx:react import SlAnimatedImage from '@shoelace-style/shoelace/dist/react/animated-image'; const css = ` .animated-image-custom-control-box::part(control-box) { top: auto; right: auto; bottom: 1rem; left: 1rem; background-color: deeppink; border: none; color: pink; } `; const App = () => ( <> ); ``` ================================================ FILE: docs/pages/components/animation.md ================================================ --- meta: title: Animation description: Animate elements declaratively with nearly 100 baked-in presets, or roll your own with custom keyframes. layout: component --- To animate an element, wrap it in `` and set an animation `name`. The animation will not start until you add the `play` attribute. Refer to the [properties table](#properties) for a list of all animation options. ```html:preview
      ``` ```jsx:react import SlAnimation from '@shoelace-style/shoelace/dist/react/animation'; const css = ` .animation-overview .box { display: inline-block; width: 100px; height: 100px; background-color: var(--sl-color-primary-600); margin: 1.5rem; } `; const App = () => ( <>
      ); ``` :::tip The animation will only be applied to the first child element found in ``. ::: ## Examples ### Animations & Easings This example demonstrates all of the baked-in animations and easings. Animations are based on those found in the popular [Animate.css](https://animate.style/) library. ```html:preview
      ``` ### Using Intersection Observer Use an [Intersection Observer](https://developer.mozilla.org/en-US/docs/Web/API/Intersection_Observer_API) to control the animation when an element enters or exits the viewport. For example, scroll the box below in and out of your screen. The animation stops when the box exits the viewport and restarts each time it enters the viewport. ```html:preview
      ``` ```jsx:react import { useEffect, useRef, useState } from 'react'; import SlAnimation from '@shoelace-style/shoelace/dist/react/animation'; const css = ` .animation-scroll { height: calc(100vh + 100px); } .animation-scroll .box { display: inline-block; width: 100px; height: 100px; background-color: var(--sl-color-primary-600); } `; const App = () => { const animation = useRef(null); const box = useRef(null); useEffect(() => { const observer = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { animation.current.play = true; } else { animation.current.play = false; animation.current.currentTime = 0; } }); if (box.current) { observer.observe(box.current); } }, [box]); return ( <>
      ); }; ``` ### Custom Keyframe Formats Supply your own [keyframe formats](https://developer.mozilla.org/en-US/docs/Web/API/Web_Animations_API/Keyframe_Formats) to build custom animations. ```html:preview
      ``` ```jsx:react import SlAnimation from '@shoelace-style/shoelace/dist/react/animation'; const css = ` .animation-keyframes .box { width: 100px; height: 100px; background-color: var(--sl-color-primary-600); } `; const App = () => ( <>
      ); ``` ### Playing Animations on Demand Animations won't play until you apply the `play` attribute. You can omit it initially, then apply it on demand such as after a user interaction. In this example, the button will animate once every time the button is clicked. ```html:preview
      Click me
      ``` ```jsx:react import { useState } from 'react'; import SlAnimation from '@shoelace-style/shoelace/dist/react/animation'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; const App = () => { const [play, setPlay] = useState(false); return (
      setPlay(false)}> setPlay(true)}> Click me
      ); }; ``` ================================================ FILE: docs/pages/components/avatar.md ================================================ --- meta: title: Avatar description: Avatars are used to represent a person or object. layout: component --- By default, a generic icon will be shown. You can personalize avatars by adding custom icons, initials, and images. You should always provide a `label` for assistive devices. ```html:preview ``` ```jsx:react import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar'; const App = () => ; ``` ## Examples ### Images To use an image for the avatar, set the `image` and `label` attributes. This will take priority and be shown over initials and icons. Avatar images can be lazily loaded by setting the `loading` attribute to `lazy`. ```html:preview ``` ```jsx:react import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar'; const App = () => ( ); ``` ### Initials When you don't have an image to use, you can set the `initials` attribute to show something more personalized than an icon. ```html:preview ``` ```jsx:react import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar'; const App = () => ; ``` ### Custom Icons When no image or initials are set, an icon will be shown. The default avatar shows a generic "user" icon, but you can customize this with the `icon` slot. ```html:preview ``` ```jsx:react import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => ( <> ); ``` ### Shapes Avatars can be shaped using the `shape` attribute. ```html:preview ``` ```jsx:react import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => ( <> ); ``` ### Avatar Groups You can group avatars with a few lines of CSS. ```html:preview
      ``` ```jsx:react import SlAvatar from '@shoelace-style/shoelace/dist/react/avatar'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const css = ` .avatar-group sl-avatar:not(:first-of-type) { margin-left: -1rem; } .avatar-group sl-avatar::part(base) { border: solid 2px var(--sl-color-neutral-0); } `; const App = () => ( <>
      ); ``` ================================================ FILE: docs/pages/components/badge.md ================================================ --- meta: title: Badge description: Badges are used to draw attention and display statuses or counts. layout: component --- ```html:preview Badge ``` ```jsx:react import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; const App = () => Badge; ``` ## Examples ### Variants Set the `variant` attribute to change the badge's variant. ```html:preview Primary Success Neutral Warning Danger ``` ```jsx:react import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; const App = () => ( <> Primary Success Neutral Warning Danger ); ``` ### Pill Badges Use the `pill` attribute to give badges rounded edges. ```html:preview Primary Success Neutral Warning Danger ``` ```jsx:react import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; const App = () => ( <> Primary Success Neutral Warning Danger ); ``` ### Pulsating Badges Use the `pulse` attribute to draw attention to the badge with a subtle animation. ```html:preview
      1 1 1 1 1
      ``` ```jsx:react import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; const css = ` .badge-pulse sl-badge:not(:last-of-type) { margin-right: 1rem; } `; const App = () => ( <>
      1 1 1 1 1
      ); ``` ### With Buttons One of the most common use cases for badges is attaching them to buttons. To make this easier, badges will be automatically positioned at the top-right when they're a child of a button. ```html:preview Requests 30 Warnings 8 Errors 6 ``` {% raw %} ```jsx:react import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; const App = () => ( <> Requests 30 Warnings 8 Errors 6 ); ``` {% endraw %} ### With Menu Items When including badges in menu items, use the `suffix` slot to make sure they're aligned correctly. ```html:preview Messages Comments 4 Replies 12 ``` {% raw %} ```jsx:react import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; import SlMenuLabel from '@shoelace-style/shoelace/dist/react/menu-label'; const App = () => ( Messages Comments 4 Replies 12 ); ``` {% endraw %} ================================================ FILE: docs/pages/components/breadcrumb-item.md ================================================ --- meta: title: Breadcrumb Item description: Breadcrumb Items are used inside breadcrumbs to represent different links. layout: component --- ```html:preview Home Clothing Shirts ``` ```jsx:react import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb'; import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => ( Home Clothing Shirts ); ``` :::tip Additional demonstrations can be found in the [breadcrumb examples](/components/breadcrumb). ::: ================================================ FILE: docs/pages/components/breadcrumb.md ================================================ --- meta: title: Breadcrumb description: Breadcrumbs provide a group of links so users can easily navigate a website's hierarchy. layout: component --- Breadcrumbs are usually placed before a page's main content with the current page shown last to indicate the user's position in the navigation. ```html:preview Catalog Clothing Women's Shirts & Tops ``` ```jsx:react import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb'; import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item'; const App = () => ( Catalog Clothing Women's Shirts & Tops ); ``` ## Examples ### Breadcrumb Links By default, breadcrumb items are rendered as buttons so you can use them to navigate single-page applications. In this case, you'll need to add event listeners to handle clicks. For websites, you'll probably want to use links instead. You can make any breadcrumb item a link by applying an `href` attribute to it. Now, when the user activates it, they'll be taken to the corresponding page — no event listeners required. ```html:preview Homepage Our Services Digital Media Web Design ``` ```jsx:react import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb'; import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item'; const App = () => ( Homepage Our Services Digital Media Web Design ); ``` ### Custom Separators Use the `separator` slot to change the separator that goes between breadcrumb items. Icons work well, but you can also use text or an image. ```html:preview First Second Third
      First Second Third
      / First Second Third ``` ```jsx:react import '@shoelace-style/shoelace/dist/components/icon/icon.js'; import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb'; import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item'; const App = () => ( <> First Second Third
      First Second Third
      / First Second Third ); ``` ### Prefixes Use the `prefix` slot to add content before any breadcrumb item. ```html:preview Home Articles Traveling ``` ```jsx:react import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb'; import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => ( Home Articles Traveling ); ``` ### Suffixes Use the `suffix` slot to add content after any breadcrumb item. ```html:preview Documents Policies Security ``` ```jsx:react import SlBreadcrumb from '@shoelace-style/shoelace/dist/react/breadcrumb'; import SlBreadcrumbItem from '@shoelace-style/shoelace/dist/react/breadcrumb-item'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => ( Documents Policies Security ); ``` ### With Dropdowns Dropdown menus can be placed in the default slot to provide additional options. ```html:preview Homepage Web Design Web Development Marketing Our Services Digital Media ``` ```jsx:react import { SlBreadcrumb, SlBreadcrumbItem, SlButton, SlDropdown, SlIcon, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react'; const App = () => ( Homepage Web Design Web Development Marketing Our Services Digital Media ); ``` Alternatively, you can place dropdown menus in a prefix or suffix slot. ```html:preview Homepage Our Services Digital Media Web Design Web Design Web Development Marketing ``` ```jsx:react import { SlBreadcrumb, SlBreadcrumbItem, SlButton, SlDropdown, SlIcon, SlMenu, SlMenuItem } from '@shoelace-style/shoelace/dist/react'; const App = () => ( Homepage Our Services Digital Media Web Design Web Design Web Development Marketing ); ``` ================================================ FILE: docs/pages/components/button-group.md ================================================ --- meta: title: Button Group description: Button groups can be used to group related buttons into sections. layout: component --- ```html:preview Left Center Right ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; const App = () => ( Left Center Right ); ``` ## Examples ### Button Sizes All button sizes are supported, but avoid mixing sizes within the same button group. ```html:preview Left Center Right

      Left Center Right

      Left Center Right ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; const App = () => ( <> Left Center Right

      Left Center Right

      Left Center Right ); ``` ### Theme Buttons Theme buttons are supported through the button's `variant` attribute. ```html:preview Left Center Right

      Left Center Right

      Left Center Right

      Left Center Right

      Left Center Right ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; const App = () => ( <> Left Center Right

      Left Center Right

      Left Center Right

      Left Center Right

      Left Center Right ); ``` ### Pill Buttons Pill buttons are supported through the button's `pill` attribute. ```html:preview Left Center Right

      Left Center Right

      Left Center Right ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; const App = () => ( <> Left Center Right

      Left Center Right

      Left Center Right ); ``` ### Dropdowns in Button Groups Dropdowns can be placed inside button groups as long as the trigger is an `` element. ```html:preview Button Button Dropdown Item 1 Item 2 Item 3 ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Button Button Dropdown Item 1 Item 2 Item 3 ); ``` ### Split Buttons Create a split button using a button and a dropdown. Use a [visually hidden](/components/visually-hidden) label to ensure the dropdown is accessible to users with assistive devices. ```html:preview Save More options Save Save as… Save all ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Save Save Save as… Save all ); ``` ### Tooltips in Button Groups Buttons can be wrapped in tooltips to provide more detail when the user interacts with them. ```html:preview Left Center Right ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip'; const App = () => ( <> Left Center Right ); ``` ### Toolbar Example Create interactive toolbars with button groups. ```html:preview
      ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlButtonGroup from '@shoelace-style/shoelace/dist/react/button-group'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip'; const css = ` .button-group-toolbar sl-button-group:not(:last-of-type) { margin-right: var(--sl-spacing-x-small); } `; const App = () => ( <>
      ); ``` ================================================ FILE: docs/pages/components/button.md ================================================ --- meta: title: Button description: Buttons represent actions that are available to the user. layout: component --- ```html:preview Button ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; const App = () => Button; ``` ## Examples ### Variants Use the `variant` attribute to set the button's variant. ```html:preview Default Primary Success Neutral Warning Danger ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; const App = () => ( <> Default Primary Success Neutral Warning Danger ); ``` ### Sizes Use the `size` attribute to change a button's size. ```html:preview Small Medium Large ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; const App = () => ( <> Small Medium Large ); ``` ### Outline Buttons Use the `outline` attribute to draw outlined buttons with transparent backgrounds. ```html:preview Default Primary Success Neutral Warning Danger ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; const App = () => ( <> Default Primary Success Neutral Warning Danger ); ``` ### Pill Buttons Use the `pill` attribute to give buttons rounded edges. ```html:preview Small Medium Large ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; const App = () => ( <> Small Medium Large ); ``` ### Circle Buttons Use the `circle` attribute to create circular icon buttons. When this attribute is set, the button expects a single `` in the default slot. ```html:preview ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => ( <> ); ``` ### Text Buttons Use the `text` variant to create text buttons that share the same size as regular buttons but don't have backgrounds or borders. ```html:preview Text Text Text ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; const App = () => ( <> Text Text Text ); ``` ### Link Buttons It's often helpful to have a button that works like a link. This is possible by setting the `href` attribute, which will make the component render an `` under the hood. This gives you all the default link behavior the browser provides (e.g. [[CMD/CTRL/SHIFT]] + [[CLICK]]) and exposes the `target` and `download` attributes. ```html:preview Link New Window Download Disabled ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; const App = () => ( <> Link New Window Download Disabled ); ``` :::tip When a `target` is set, the link will receive `rel="noreferrer noopener"` for [security reasons](https://mathiasbynens.github.io/rel-noopener/). ::: ### Setting a Custom Width As expected, buttons can be given a custom width by passing inline styles to the component (or using a class). This is useful for making buttons span the full width of their container on smaller screens. ```html:preview Small Medium Large ``` {% raw %} ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; const App = () => ( <> Small Medium Large ); ``` {% endraw %} ### Prefix and Suffix Icons Use the `prefix` and `suffix` slots to add icons. ```html:preview Settings Refresh Open

      Settings Refresh Open

      Settings Refresh Open ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => ( <> Settings Refresh Open

      Settings Refresh Open

      Settings Refresh Open ); ``` ### Caret Use the `caret` attribute to add a dropdown indicator when a button will trigger a dropdown, menu, or popover. ```html:preview Small Medium Large ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; const App = () => ( <> Small Medium Large ); ``` ### Loading Use the `loading` attribute to make a button busy. The width will remain the same as before, preventing adjacent elements from moving around. ```html:preview Default Primary Success Neutral Warning Danger ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; const App = () => ( <> Default Primary Success Neutral Warning Danger ); ``` ### Disabled Use the `disabled` attribute to disable a button. ```html:preview Default Primary Success Neutral Warning Danger ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; const App = () => ( <> Default Primary Success Neutral Warning Danger ); ``` ### Styling Buttons This example demonstrates how to style buttons using a custom class. This is the recommended approach if you need to add additional variations. To customize an existing variation, modify the selector to target the button's `variant` attribute instead of a class (e.g. `sl-button[variant="primary"]`). ```html:preview Pink Button ``` ================================================ FILE: docs/pages/components/card.md ================================================ --- meta: title: Card description: Cards can be used to group related subjects in a container. layout: component --- ```html:preview A kitten sits patiently between a terracotta pot and decorative grasses. Mittens
      This kitten is as cute as he is playful. Bring him home today!
      6 weeks old
      More Info
      ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlCard from '@shoelace-style/shoelace/dist/react/card'; import SlRating from '@shoelace-style/shoelace/dist/react/rating'; const css = ` .card-overview { max-width: 300px; } .card-overview small { color: var(--sl-color-neutral-500); } .card-overview [slot="footer"] { display: flex; justify-content: space-between; align-items: center; } `; const App = () => ( <> A kitten sits patiently between a terracotta pot and decorative grasses. Mittens
      This kitten is as cute as he is playful. Bring him home today!
      6 weeks old
      More Info
      ); ``` ## Examples ### Basic Card Basic cards aren't very exciting, but they can display any content you want them to. ```html:preview This is just a basic card. No image, no header, and no footer. Just your content. ``` ```jsx:react import SlCard from '@shoelace-style/shoelace/dist/react/card'; const css = ` .card-basic { max-width: 300px; } `; const App = () => ( <> This is just a basic card. No image, no header, and no footer. Just your content. ); ``` ### Card with Header Headers can be used to display titles and more. ```html:preview
      Header Title
      This card has a header. You can put all sorts of things in it!
      ``` ```jsx:react import SlCard from '@shoelace-style/shoelace/dist/react/card'; import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; const css = ` .card-header { max-width: 300px; } .card-header [slot="header"] { display: flex; align-items: center; justify-content: space-between; } .card-header h3 { margin: 0; } .card-header sl-icon-button { font-size: var(--sl-font-size-medium); } `; const App = () => ( <>
      Header Title
      This card has a header. You can put all sorts of things in it!
      ); ``` ### Card with Footer Footers can be used to display actions, summaries, or other relevant content. ```html:preview This card has a footer. You can put all sorts of things in it!
      Preview
      ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlCard from '@shoelace-style/shoelace/dist/react/card'; import SlRating from '@shoelace-style/shoelace/dist/react/rating'; const css = ` .card-footer { max-width: 300px; } .card-footer [slot="footer"] { display: flex; justify-content: space-between; align-items: center; } `; const App = () => ( <> This card has a footer. You can put all sorts of things in it!
      Preview
      ); ``` ### Images Cards accept an `image` slot. The image is displayed atop the card and stretches to fit. ```html:preview A kitten walks towards camera on top of pallet. This is a kitten, but not just any kitten. This kitten likes walking along pallets. ``` ```jsx:react import SlCard from '@shoelace-style/shoelace/dist/react/card'; const css = ` .card-image { max-width: 300px; } `; const App = () => ( <> A kitten walks towards camera on top of pallet. This is a kitten, but not just any kitten. This kitten likes walking along pallets. ); ``` ================================================ FILE: docs/pages/components/carousel-item.md ================================================ --- meta: title: Carousel Item description: A carousel item represent a slide within a carousel. layout: component --- ```html:preview The sun shines on the mountains and trees - Photo by Adam Kool on Unsplash A waterfall in the middle of a forest - Photo by Thomas Kelly on Unsplash The sun is setting over a lavender field - Photo by Leonard Cotte on Unsplash A field of grass with the sun setting in the background - Photo by Sapan Patel on Unsplash A scenic view of a mountain with clouds rolling in - Photo by V2osk on Unsplash ``` ```jsx:react import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; const App = () => ( The sun shines on the mountains and trees - Photo by Adam Kool on Unsplash A waterfall in the middle of a forest - Photo by Thomas Kelly on Unsplash The sun is setting over a lavender field - Photo by Leonard Cotte on Unsplash A field of grass with the sun setting in the background - Photo by Sapan Patel on Unsplash A scenic view of a mountain with clouds rolling in - Photo by V2osk on Unsplash ); ``` :::tip Additional demonstrations can be found in the [carousel examples](/components/carousel). ::: ================================================ FILE: docs/pages/components/carousel.md ================================================ --- meta: title: Carousel description: Carousels display an arbitrary number of content slides along a horizontal or vertical axis. layout: component --- ```html:preview The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ``` ```jsx:react import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; const App = () => ( <> The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ); ``` ## Examples ### Pagination Use the `pagination` attribute to show the total number of slides and the current slide as a set of interactive dots. ```html:preview The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ``` ```jsx:react import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; const App = () => ( The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ); ``` ### Navigation Use the `navigation` attribute to show previous and next buttons. ```html:preview The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ``` ```jsx:react import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; const App = () => ( The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ); ``` ### Looping By default, the carousel will not advanced beyond the first and last slides. You can change this behavior and force the carousel to "wrap" with the `loop` attribute. ```html:preview The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ``` ```jsx:react import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; const App = () => ( The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ); ``` ### Autoplay The carousel will automatically advance when the `autoplay` attribute is used. To change how long a slide is shown before advancing, set `autoplay-interval` to the desired number of milliseconds. For best results, use the `loop` attribute when autoplay is enabled. Note that autoplay will pause while the user interacts with the carousel. ```html:preview The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ``` ```jsx:react import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; const App = () => ( The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ); ``` ### Mouse Dragging The carousel uses [scroll snap](https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Scroll_Snap) to position slides at various snap positions. This allows users to scroll through the slides very naturally, especially on touch devices. Unfortunately, desktop users won't be able to click and drag with a mouse, which can feel unnatural. Adding the `mouse-dragging` attribute can help with this. This example is best demonstrated using a mouse. Try clicking and dragging the slide to move it. Then toggle the switch and try again. ```html:preview
      The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) Enable mouse dragging
      ``` ```jsx:react import { useState } from 'react'; import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlSwitch from '@shoelace-style/shoelace/dist/react/switch'; const App = () => { const [isEnabled, setIsEnabled] = useState(false); return ( <> The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) setIsEnabled(!isEnabled)}> Enable mouse dragging ); }; ``` ### Multiple Slides Per View The `slides-per-page` attribute makes it possible to display multiple slides at a time. You can also use the `slides-per-move` attribute to advance more than once slide at a time, if desired. ```html:preview Slide 1 Slide 2 Slide 3 Slide 4 Slide 5 Slide 6 ``` {% raw %} ```jsx:react import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; const App = () => ( Slide 1 Slide 2 Slide 3 Slide 4 Slide 5 Slide 6 ); ``` {% endraw %} ### Adding and Removing Slides The content of the carousel can be changed by adding or removing carousel items. The carousel will update itself automatically. ```html:preview Slide 1 Slide 2 Slide 3 ``` {% raw %} ```jsx:react import { useState } from 'react'; import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; const css = ` .dynamic-carousel { --aspect-ratio: 3 / 2; } .dynamic-carousel ~ .carousel-options { display: flex; justify-content: center; margin-top: var(--sl-spacing-large); } .dynamic-carousel sl-carousel-item { flex: 0 0 100%; display: flex; align-items: center; justify-content: center; color: white; font-size: var(--sl-font-size-2x-large); } `; const App = () => { const [slides, setSlides] = useState(['#204ed8', '#be133d', '#6e28d9']); const colors = ['red', 'orange', 'yellow', 'green', 'blue', 'violet']; const addSlide = () => { setSlides([...slides, getRandomColor()]); }; const removeSlide = () => { setSlides(slides.slice(0, -1)); }; return ( <> {slides.map((color, i) => ( Slide {i} ))}
      Add slide Remove slide
      ); }; ``` {% endraw %} ### Vertical Scrolling Setting the `orientation` attribute to `vertical` will render the carousel in a vertical layout. If the content of your slides vary in height, you will need to set amn explicit `height` or `max-height` on the carousel using CSS. ```html:preview The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ``` ```jsx:react import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; const css = ` .vertical { max-height: 400px; } .vertical::part(base) { grid-template-areas: 'slides slides pagination'; } .vertical::part(pagination) { flex-direction: column; } .vertical::part(navigation) { transform: rotate(90deg); display: flex; } `; const App = () => ( <> The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ); ``` ### Aspect Ratio Use the `--aspect-ratio` custom property to customize the size of the carousel's viewport from the default value of 16/9. ```html:preview The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) 1/1 3/2 16/9 ``` {% raw %} ```jsx:react import { useState } from 'react'; import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlSelect from '@shoelace-style/shoelace/dist/react/select'; import SlOption from '@shoelace-style/shoelace/dist/react/option'; const App = () => { const [aspectRatio, setAspectRatio] = useState('3/2'); return ( <> The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) setAspectRatio(event.target.value)} > 1 / 1 3 / 2 16 / 9 ); }; ``` {% endraw %} ### Scroll Hint Use the `--scroll-hint` custom property to add inline padding in horizontal carousels and block padding in vertical carousels. This will make the closest slides slightly visible, hinting that there are more items in the carousel. ```html:preview The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ``` {% raw %} ```jsx:react import { useState } from 'react'; import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlRange from '@shoelace-style/shoelace/dist/react/range'; const App = () => ( <> The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash) ); ``` {% endraw %} ### Gallery Example The carousel has a robust API that makes it possible to extend and customize. This example syncs the active slide with a set of thumbnails, effectively creating a gallery-style carousel. ```html:preview The sun shines on the mountains and trees (by Adam Kool on Unsplash) A waterfall in the middle of a forest (by Thomas Kelly on Unsplash) The sun is setting over a lavender field (by Leonard Cotte on Unsplash) A field of grass with the sun setting in the background (by Sapan Patel on Unsplash) A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash)
      Thumbnail by 1 Thumbnail by 2 Thumbnail by 3 Thumbnail by 4 Thumbnail by 5
      ``` ```jsx:react import { useRef } from 'react'; import SlCarousel from '@shoelace-style/shoelace/dist/react/carousel'; import SlCarouselItem from '@shoelace-style/shoelace/dist/react/carousel-item'; import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlRange from '@shoelace-style/shoelace/dist/react/range'; const css = ` .carousel-thumbnails { --slide-aspect-ratio: 3 / 2; } .thumbnails { display: flex; justify-content: center; } .thumbnails__scroller { display: flex; gap: var(--sl-spacing-small); overflow-x: auto; scrollbar-width: none; scroll-behavior: smooth; scroll-padding: var(--sl-spacing-small); } .thumbnails__scroller::-webkit-scrollbar { display: none; } .thumbnails__image { width: 64px; height: 64px; object-fit: cover; opacity: 0.3; will-change: opacity; transition: 250ms opacity; cursor: pointer; } .thumbnails__image.active { opacity: 1; } `; const images = [ { src: '/assets/examples/carousel/mountains.jpg', alt: 'The sun shines on the mountains and trees (by Adam Kool on Unsplash' }, { src: '/assets/examples/carousel/waterfall.jpg', alt: 'A waterfall in the middle of a forest (by Thomas Kelly on Unsplash' }, { src: '/assets/examples/carousel/sunset.jpg', alt: 'The sun is setting over a lavender field (by Leonard Cotte on Unsplash' }, { src: '/assets/examples/carousel/field.jpg', alt: 'A field of grass with the sun setting in the background (by Sapan Patel on Unsplash' }, { src: '/assets/examples/carousel/valley.jpg', alt: 'A scenic view of a mountain with clouds rolling in (by V2osk on Unsplash' } ]; const App = () => { const carouselRef = useRef(); const thumbnailsRef = useRef(); const [currentSlide, setCurrentSlide] = useState(0); useEffect(() => { const thumbnails = Array.from(thumbnailsRef.current.querySelectorAll('.thumbnails__image')); thumbnails[currentSlide]..scrollIntoView({ block: 'nearest' }); }, [currentSlide]); const handleThumbnailClick = (index) => { carouselRef.current.goToSlide(index); } const handleSlideChange = (event) => { const slideIndex = e.detail.index; setCurrentSlide(slideIndex); } return ( <> {images.map({ src, alt }) => ( {alt} )}
      {images.map({ src, alt }, i) => ( {`Thumbnail handleThumbnailClick(i)} src={src} /> )}
      ); }; ``` ================================================ FILE: docs/pages/components/checkbox.md ================================================ --- meta: title: Checkbox description: Checkboxes allow the user to toggle an option on or off. layout: component --- ```html:preview Checkbox ``` ```jsx:react import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; const App = () => Checkbox; ``` :::tip This component works with standard `
      ` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. ::: ## Examples ### Checked Use the `checked` attribute to activate the checkbox. ```html:preview Checked ``` ```jsx:react import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; const App = () => Checked; ``` ### Indeterminate Use the `indeterminate` attribute to make the checkbox indeterminate. ```html:preview Indeterminate ``` ```jsx:react import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; const App = () => Indeterminate; ``` ### Disabled Use the `disabled` attribute to disable the checkbox. ```html:preview Disabled ``` ```jsx:react import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; const App = () => Disabled; ``` ### Sizes Use the `size` attribute to change a checkbox's size. ```html:preview Small
      Medium
      Large ``` ```jsx:react import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; const App = () => ( <> Small
      Medium
      Large ); ``` ### Help Text Add descriptive help text to a switch with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead. ```html:preview Label ``` ```jsx:react import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; const App = () => Label; ``` ### Custom Validity Use the `setCustomValidity()` method to set a custom validation message. This will prevent the form from submitting and make the browser display the error message you provide. To clear the error, call this function with an empty string. ```html:preview Check me
      Submit
      ``` {% raw %} ```jsx:react import { useEffect, useRef } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlCheckbox from '@shoelace-style/shoelace/dist/react/checkbox'; const App = () => { const checkbox = useRef(null); const errorMessage = `Don't forget to check me!`; function handleChange() { checkbox.current.setCustomValidity(checkbox.current.checked ? '' : errorMessage); } function handleSubmit(event) { event.preventDefault(); alert('All fields are valid!'); } useEffect(() => { checkbox.current.setCustomValidity(errorMessage); }, []); return (
      Check me
      Submit
      ); }; ``` {% endraw %} ================================================ FILE: docs/pages/components/color-picker.md ================================================ --- meta: title: Color Picker description: Color pickers allow the user to select a color. layout: component --- ```html:preview ``` ```jsx:react import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; const App = () => ; ``` :::tip This component works with standard `
      ` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. ::: ## Examples ### Initial Value Use the `value` attribute to set an initial value for the color picker. ```html:preview ``` ```jsx:react import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; const App = () => ; ``` ### Opacity Use the `opacity` attribute to enable the opacity slider. When this is enabled, the value will be displayed as HEXA, RGBA, HSLA, or HSVA based on `format`. ```html:preview ``` ```jsx:react import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; const App = () => ; ``` ### Formats Set the color picker's format with the `format` attribute. Valid options include `hex`, `rgb`, `hsl`, and `hsv`. Note that the color picker's input will accept any parsable format (including CSS color names) regardless of this option. To prevent users from toggling the format themselves, add the `no-format-toggle` attribute. ```html:preview ``` ```jsx:react import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; const App = () => ( <> ); ``` ### Swatches Use the `swatches` attribute to add convenient presets to the color picker. Any format the color picker can parse is acceptable (including CSS color names), but each value must be separated by a semicolon (`;`). Alternatively, you can pass an array of color values to this property using JavaScript. ```html:preview ``` ```jsx:react import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; const App = () => ( ); ``` ### Sizes Use the `size` attribute to change the color picker's trigger size. ```html:preview ``` ```jsx:react import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; const App = () => ( <> ); ``` ### Inline The color picker can be rendered inline instead of in a dropdown using the `inline` attribute. ```html:preview ``` ```jsx:react import SlColorPicker from '@shoelace-style/shoelace/dist/react/color-picker'; const App = () => ; ``` ================================================ FILE: docs/pages/components/copy-button.md ================================================ --- meta: title: Copy Button description: Copies data to the clipboard when the user clicks the button. layout: component --- ```html:preview ``` ```jsx:react import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; const App = () => ( ); ``` ## Examples ### Custom Labels Copy Buttons display feedback in a tooltip. You can customize the labels using the `copy-label`, `success-label`, and `error-label` attributes. ```html:preview ``` ```jsx:react import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; const App = () => ( ); ``` ### Custom Icons Use the `copy-icon`, `success-icon`, and `error-icon` slots to customize the icons that get displayed for each state. You can use [``](/components/icon) or your own images. ```html:preview ``` ```jsx:react import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; import { SlIcon } from '@shoelace-style/shoelace/dist/react/icon'; const App = () => ( <> ); ``` ### Copying Values From Other Elements Normally, the data that gets copied will come from the component's `value` attribute, but you can copy data from any element within the same document by providing its `id` to the `from` attribute. When using the `from` attribute, the element's [`textContent`](https://developer.mozilla.org/en-US/docs/Web/API/Node/textContent) will be copied by default. Passing an attribute or property modifier will let you copy data from one of the element's attributes or properties instead. To copy data from an attribute, use `from="id[attr]"` where `id` is the id of the target element and `attr` is the name of the attribute you'd like to copy. To copy data from a property, use `from="id.prop"` where `id` is the id of the target element and `prop` is the name of the property you'd like to copy. ```html:preview +1 (234) 456-7890



      Shoelace Website ``` ```jsx:react import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; import { SlInput } from '@shoelace-style/shoelace/dist/react/input'; const App = () => ( <> {/* Copies the span's textContent */} +1 (234) 456-7890

      {/* Copies the input's "value" property */}

      {/* Copies the link's "href" attribute */} Shoelace Website ); ``` ### Handling Errors A copy error will occur if the value is an empty string, if the `from` attribute points to an id that doesn't exist, or if the browser rejects the operation for any reason. When this happens, the `sl-error` event will be emitted. This example demonstrates what happens when a copy error occurs. You can customize the error label and icon using the `error-label` attribute and the `error-icon` slot, respectively. ```html:preview ``` ```jsx:react import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; const App = () => ( ); ``` ### Disabled Copy buttons can be disabled by adding the `disabled` attribute. ```html:preview ``` ```jsx:react import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; const App = () => ( ); ``` ### Changing Feedback Duration A success indicator is briefly shown after copying. You can customize the length of time the indicator is shown using the `feedback-duration` attribute. ```html:preview ``` ```jsx:react import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; const App = () => ( ); ``` ### Custom Styles You can customize the button to your liking with CSS. ```html:preview ``` ```jsx:react import { SlCopyButton } from '@shoelace-style/shoelace/dist/react/copy-button'; const css = ` .custom-styles { --success-color: white; --error-color: white; color: white; } .custom-styles::part(button) { background-color: #ff1493; border: solid 4px #ff7ac1; border-right-color: #ad005c; border-bottom-color: #ad005c; border-radius: 0; transition: 100ms scale ease-in-out, 100ms translate ease-in-out; } .custom-styles::part(button):hover { scale: 1.1; } .custom-styles::part(button):active { translate: 0 2px; } .custom-styles::part(button):focus-visible { outline: dashed 2px deeppink; outline-offset: 4px; } `; const App = () => ( <> ); ``` ================================================ FILE: docs/pages/components/details.md ================================================ --- meta: title: Details description: Details show a brief summary and expand to show additional content. layout: component --- ```html:preview Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ``` ```jsx:react import SlDetails from '@shoelace-style/shoelace/dist/react/details'; const App = () => ( Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ); ``` ## Examples ### Disabled Use the `disable` attribute to prevent the details from expanding. ```html:preview Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ``` ```jsx:react import SlDetails from '@shoelace-style/shoelace/dist/react/details'; const App = () => ( Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ); ``` ### Customizing the Summary Icon Use the `expand-icon` and `collapse-icon` slots to change the expand and collapse icons, respectively. To disable the animation, override the `rotate` property on the `summary-icon` part as shown below. ```html:preview Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ``` ```jsx:react import SlDetails from '@shoelace-style/shoelace/dist/react/details'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const css = ` sl-details.custom-icon::part(summary-icon) { /* Disable the expand/collapse animation */ rotate: none; } `; const App = () => ( <> Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. ); ``` ### Grouping Details Details are designed to function independently, but you can simulate a group or "accordion" where only one is shown at a time by listening for the `sl-show` event. ```html:preview
      Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.
      ``` ================================================ FILE: docs/pages/components/dialog.md ================================================ --- meta: title: Dialog description: 'Dialogs, sometimes called "modals", appear above the page and require the user''s immediate attention.' layout: component --- ```html:preview Lorem ipsum dolor sit amet, consectetur adipiscing elit. Close Open Dialog ``` ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDialog from '@shoelace-style/shoelace/dist/react/dialog'; const App = () => { const [open, setOpen] = useState(false); return ( <> setOpen(false)}> Lorem ipsum dolor sit amet, consectetur adipiscing elit. setOpen(false)}> Close setOpen(true)}>Open Dialog ); }; ``` ## Examples ### Custom Width Use the `--width` custom property to set the dialog's width. ```html:preview Lorem ipsum dolor sit amet, consectetur adipiscing elit. Close Open Dialog ``` {% raw %} ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDialog from '@shoelace-style/shoelace/dist/react/dialog'; const App = () => { const [open, setOpen] = useState(false); return ( <> setOpen(false)}> Lorem ipsum dolor sit amet, consectetur adipiscing elit. setOpen(false)}> Close setOpen(true)}>Open Dialog ); }; ``` {% endraw %} ### Scrolling By design, a dialog's height will never exceed that of the viewport. As such, dialogs will not scroll with the page ensuring the header and footer are always accessible to the user. ```html:preview

      Scroll down and give it a try! 👇

      Close
      Open Dialog ``` {% raw %} ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDialog from '@shoelace-style/shoelace/dist/react/dialog'; const App = () => { const [open, setOpen] = useState(false); return ( <> setOpen(false)}>

      Scroll down and give it a try! 👇

      setOpen(false)}> Close
      setOpen(true)}>Open Dialog ); }; ``` {% endraw %} ### Header Actions The header shows a functional close button by default. You can use the `header-actions` slot to add additional [icon buttons](/components/icon-button) if needed. ```html:preview Lorem ipsum dolor sit amet, consectetur adipiscing elit. Close Open Dialog ``` ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDialog from '@shoelace-style/shoelace/dist/react/dialog'; import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; const App = () => { const [open, setOpen] = useState(false); return ( <> setOpen(false)}> window.open(location.href)} /> Lorem ipsum dolor sit amet, consectetur adipiscing elit. setOpen(false)}> Close setOpen(true)}>Open Dialog ); }; ``` ### Preventing the Dialog from Closing By default, dialogs will close when the user clicks the close button, clicks the overlay, or presses the [[Escape]] key. In most cases, the default behavior is the best behavior in terms of UX. However, there are situations where this may be undesirable, such as when data loss will occur. To keep the dialog open in such cases, you can cancel the `sl-request-close` event. When canceled, the dialog will remain open and pulse briefly to draw the user's attention to it. You can use `event.detail.source` to determine what triggered the request to close. This example prevents the dialog from closing when the overlay is clicked, but allows the close button or [[Escape]] to dismiss it. ```html:preview This dialog will not close when you click on the overlay. Close Open Dialog ``` ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDialog from '@shoelace-style/shoelace/dist/react/dialog'; const App = () => { const [open, setOpen] = useState(false); // Prevent the dialog from closing when the user clicks on the overlay function handleRequestClose(event) { if (event.detail.source === 'overlay') { event.preventDefault(); } } return ( <> setOpen(false)}> This dialog will not close when you click on the overlay. setOpen(false)}> Close setOpen(true)}>Open Dialog ); }; ``` ### Customizing Initial Focus By default, the dialog's panel will gain focus when opened. This allows a subsequent tab press to focus on the first tabbable element in the dialog. If you want a different element to have focus, add the `autofocus` attribute to it as shown below. ```html:preview Close Open Dialog ``` ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDialog from '@shoelace-style/shoelace/dist/react/dialog'; import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => { const [open, setOpen] = useState(false); return ( <> setOpen(false)}> setOpen(false)}> Close setOpen(true)}>Open Dialog ); }; ``` :::tip You can further customize initial focus behavior by canceling the `sl-initial-focus` event and setting focus yourself inside the event handler. ::: ================================================ FILE: docs/pages/components/divider.md ================================================ --- meta: title: Divider description: Dividers are used to visually separate or group elements. layout: component --- ```html:preview ``` ```jsx:react import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; const App = () => ; ``` ## Examples ### Width Use the `--width` custom property to change the width of the divider. ```html:preview ``` {% raw %} ```jsx:react import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; const App = () => ; ``` {% endraw %} ### Color Use the `--color` custom property to change the color of the divider. ```html:preview ``` {% raw %} ```jsx:react import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; const App = () => ; ``` {% endraw %} ### Spacing Use the `--spacing` custom property to change the amount of space between the divider and it's neighboring elements. ```html:preview
      Above Below
      ``` {% raw %} ```jsx:react import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; const App = () => ( <> Above Below ); ``` {% endraw %} ### Vertical Add the `vertical` attribute to draw the divider in a vertical orientation. The divider will span the full height of its container. Vertical dividers work especially well inside of a flex container. ```html:preview
      First Middle Last
      ``` {% raw %} ```jsx:react import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; const App = () => (
      First Middle Last
      ); ``` {% endraw %} ### Menu Dividers Use dividers in [menus](/components/menu) to visually group menu items. ```html:preview Option 1 Option 2 Option 3 Option 4 Option 5 Option 6 ``` {% raw %} ```jsx:react import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Option 1 Option 2 Option 3 Option 4 Option 5 Option 6 ); ``` ### Tailwind users Using TailwindCSS with Shoelace [may override divider styles](https://github.com/shoelace-style/shoelace/issues/2088), making them invisible. As a workaround, add this to your Tailwind config file. ```css @layer base { sl-divider:not([vertical]) { border-top: solid var(--width) var(--color); } sl-divider[vertical] { border-left: solid var(--width) var(--color); } } ``` {% endraw %} ================================================ FILE: docs/pages/components/drawer.md ================================================ --- meta: title: Drawer description: Drawers slide in from a container to expose additional options and information. layout: component --- ```html:preview Lorem ipsum dolor sit amet, consectetur adipiscing elit. Close Open Drawer ``` ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; const App = () => { const [open, setOpen] = useState(false); return ( <> setOpen(false)}> Lorem ipsum dolor sit amet, consectetur adipiscing elit. setOpen(false)}> Close setOpen(true)}>Open Drawer ); }; ``` ## Examples ### Slide in From Start By default, drawers slide in from the end. To make the drawer slide in from the start, set the `placement` attribute to `start`. ```html:preview This drawer slides in from the start. Close Open Drawer ``` ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; const App = () => { const [open, setOpen] = useState(false); return ( <> setOpen(false)}> This drawer slides in from the start. setOpen(false)}> Close setOpen(true)}>Open Drawer ); }; ``` ### Slide in From Top To make the drawer slide in from the top, set the `placement` attribute to `top`. ```html:preview This drawer slides in from the top. Close Open Drawer ``` ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; const App = () => { const [open, setOpen] = useState(false); return ( <> setOpen(false)}> This drawer slides in from the top. setOpen(false)}> Close setOpen(true)}>Open Drawer ); }; ``` ### Slide in From Bottom To make the drawer slide in from the bottom, set the `placement` attribute to `bottom`. ```html:preview This drawer slides in from the bottom. Close Open Drawer ``` ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; const App = () => { const [open, setOpen] = useState(false); return ( <> setOpen(false)}> This drawer slides in from the bottom. setOpen(false)}> Close setOpen(true)}>Open Drawer ); }; ``` ### Contained to an Element By default, drawers slide out of their [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport. To make a drawer slide out of a parent element, add the `contained` attribute to the drawer and apply `position: relative` to its parent. Unlike normal drawers, contained drawers are not modal. This means they do not show an overlay, they do not trap focus, and they are not dismissible with [[Escape]]. This is intentional to allow users to interact with elements outside of the drawer. ```html:preview
      The drawer will be contained to this box. This content won't shift or be affected in any way when the drawer opens. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Close
      Toggle Drawer ``` {% raw %} ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; const App = () => { const [open, setOpen] = useState(false); return ( <>
      The drawer will be contained to this box. This content won't shift or be affected in any way when the drawer opens. setOpen(false)} style={{ '--size': '50%' }} > Lorem ipsum dolor sit amet, consectetur adipiscing elit. setOpen(false)}> Close
      setOpen(true)}>Open Drawer ); }; ``` {% endraw %} ### Custom Size Use the `--size` custom property to set the drawer's size. This will be applied to the drawer's width or height depending on its `placement`. ```html:preview This drawer is always 50% of the viewport. Close Open Drawer ``` {% raw %} ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; const App = () => { const [open, setOpen] = useState(false); return ( <> setOpen(false)} style={{ '--size': '50vw' }}> This drawer is always 50% of the viewport. setOpen(false)}> Close setOpen(true)}>Open Drawer ); }; ``` {% endraw %} ### Scrolling By design, a drawer's height will never exceed 100% of its container. As such, drawers will not scroll with the page to ensure the header and footer are always accessible to the user. ```html:preview

      Scroll down and give it a try! 👇

      Close
      Open Drawer ``` {% raw %} ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; const App = () => { const [open, setOpen] = useState(false); return ( <> setOpen(false)}>

      Scroll down and give it a try! 👇

      setOpen(false)}> Close
      setOpen(true)}>Open Drawer ); }; ``` {% endraw %} ### Header Actions The header shows a functional close button by default. You can use the `header-actions` slot to add additional [icon buttons](/components/icon-button) if needed. ```html:preview Lorem ipsum dolor sit amet, consectetur adipiscing elit. Close Open Drawer ``` ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; const App = () => { const [open, setOpen] = useState(false); return ( <> setOpen(false)}> window.open(location.href)} /> Lorem ipsum dolor sit amet, consectetur adipiscing elit. setOpen(false)}> Close setOpen(true)}>Open Drawer ); }; ``` ### Preventing the Drawer from Closing By default, drawers will close when the user clicks the close button, clicks the overlay, or presses the [[Escape]] key. In most cases, the default behavior is the best behavior in terms of UX. However, there are situations where this may be undesirable, such as when data loss will occur. To keep the drawer open in such cases, you can cancel the `sl-request-close` event. When canceled, the drawer will remain open and pulse briefly to draw the user's attention to it. You can use `event.detail.source` to determine what triggered the request to close. This example prevents the drawer from closing when the overlay is clicked, but allows the close button or [[Escape]] to dismiss it. ```html:preview This drawer will not close when you click on the overlay. Close Open Drawer ``` ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; const App = () => { const [open, setOpen] = useState(false); // Prevent the drawer from closing when the user clicks on the overlay function handleRequestClose(event) { if (event.detail.source === 'overlay') { event.preventDefault(); } } return ( <> setOpen(false)}> This drawer will not close when you click on the overlay. setOpen(false)}> Save & Close setOpen(true)}>Open Drawer ); }; ``` ### Customizing Initial Focus By default, the drawer's panel will gain focus when opened. This allows a subsequent tab press to focus on the first tabbable element in the drawer. If you want a different element to have focus, add the `autofocus` attribute to it as shown below. ```html:preview Close Open Drawer ``` ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDrawer from '@shoelace-style/shoelace/dist/react/drawer'; import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => { const [open, setOpen] = useState(false); return ( <> setOpen(false)}> setOpen(false)}> Close setOpen(true)}>Open Drawer ); }; ``` :::tip You can further customize initial focus behavior by canceling the `sl-initial-focus` event and setting focus yourself inside the event handler. ::: ================================================ FILE: docs/pages/components/dropdown.md ================================================ --- meta: title: Dropdown description: 'Dropdowns expose additional content that "drops down" in a panel.' layout: component --- Dropdowns consist of a trigger and a panel. By default, activating the trigger will expose the panel and interacting outside of the panel will close it. Dropdowns are designed to work well with [menus](/components/menu) to provide a list of options the user can select from. However, dropdowns can also be used in lower-level applications (e.g. [color picker](/components/color-picker)). The API gives you complete control over showing, hiding, and positioning the panel. ```html:preview Dropdown Dropdown Item 1 Dropdown Item 2 Dropdown Item 3 Checkbox Disabled Prefix Suffix Icon ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Dropdown Dropdown Item 1 Dropdown Item 2 Dropdown Item 3 Checkbox Disabled Prefix Suffix Icon ); ``` ## Examples ### Getting the Selected Item When dropdowns are used with [menus](/components/menu), you can listen for the [`sl-select`](/components/menu#events) event to determine which menu item was selected. The menu item element will be exposed in `event.detail.item`. You can set `value` props to make it easier to identify commands. ```html:preview ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => { function handleSelect(event) { const selectedItem = event.detail.item; console.log(selectedItem.value); } return ( Edit Cut Copy Paste ); }; ``` Alternatively, you can listen for the `click` event on individual menu items. Note that, using this approach, disabled menu items will still emit a `click` event. ```html:preview ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => { function handleCut() { console.log('cut'); } function handleCopy() { console.log('copy'); } function handlePaste() { console.log('paste'); } return ( Edit Cut Copy Paste ); }; ``` ### Placement The preferred placement of the dropdown can be set with the `placement` attribute. Note that the actual position may vary to ensure the panel remains in the viewport. ```html:preview Edit Cut Copy Paste Find Replace ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Edit Cut Copy Paste Find Replace ); ``` ### Distance The distance from the panel to the trigger can be customized using the `distance` attribute. This value is specified in pixels. ```html:preview Edit Cut Copy Paste Find Replace ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Edit Cut Copy Paste Find Replace ); ``` ### Skidding The offset of the panel along the trigger can be customized using the `skidding` attribute. This value is specified in pixels. ```html:preview Edit Cut Copy Paste Find Replace ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Edit Cut Copy Paste Find Replace ); ``` ### Submenus To create a submenu, nest an `` element in a [menu item](/components/menu-item). ```html:preview Edit Undo Redo Cut Copy Paste Find Find… Find Next Find Previous Transformations Make uppercase Make lowercase Capitalize ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const css = ` .dropdown-hoist { border: solid 2px var(--sl-panel-border-color); padding: var(--sl-spacing-medium); overflow: hidden; } `; const App = () => ( <> Edit Undo Redo Cut Copy Paste Find Find… Find Next Find Previous Transformations Make uppercase Make lowercase Capitalize ); ``` :::warning As a UX best practice, avoid using more than one level of submenu when possible. ::: ### Hoisting Dropdown panels will be clipped if they're inside a container that has `overflow: auto|hidden`. The `hoist` attribute forces the panel to use a fixed positioning strategy, allowing it to break out of the container. In this case, the panel will be positioned relative to its [containing block](https://developer.mozilla.org/en-US/docs/Web/CSS/Containing_block#Identifying_the_containing_block), which is usually the viewport unless an ancestor uses a `transform`, `perspective`, or `filter`. [Refer to this page](https://developer.mozilla.org/en-US/docs/Web/CSS/position#fixed) for more details. ```html:preview ``` ```jsx:react import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlDropdown from '@shoelace-style/shoelace/dist/react/dropdown'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const css = ` .dropdown-hoist { border: solid 2px var(--sl-panel-border-color); padding: var(--sl-spacing-medium); overflow: hidden; } `; const App = () => ( <>
      No Hoist Item 1 Item 2 Item 3 Hoist Item 1 Item 2 Item 3
      ); ``` ================================================ FILE: docs/pages/components/format-bytes.md ================================================ --- meta: title: Format Bytes description: Formats a number as a human readable bytes value. layout: component --- ```html:preview
      The file is in size.

      ``` {% raw %} ```jsx:react import { useState } from 'react'; import SlButton from '@shoelace-style/shoelace/dist/react/button'; import SlFormatBytes from '@shoelace-style/shoelace/dist/react/format-bytes'; import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => { const [value, setValue] = useState(1000); return ( <> The file is in size.

      setValue(event.target.value)} /> ); }; ``` {% endraw %} ## Examples ### Formatting Bytes Set the `value` attribute to a number to get the value in bytes. ```html:preview


      ``` ```jsx:react import SlFormatBytes from '@shoelace-style/shoelace/dist/react/format-bytes'; const App = () => ( <>


      ); ``` ### Formatting Bits To get the value in bits, set the `unit` attribute to `bit`. ```html:preview


      ``` ```jsx:react import SlFormatBytes from '@shoelace-style/shoelace/dist/react/format-bytes'; const App = () => ( <>


      ); ``` ### Localization Use the `lang` attribute to set the number formatting locale. ```html:preview


      ``` ```jsx:react import SlFormatBytes from '@shoelace-style/shoelace/dist/react/format-bytes'; const App = () => ( <>


      ); ``` ================================================ FILE: docs/pages/components/format-date.md ================================================ --- meta: title: Format Date description: Formats a date/time using the specified locale and options. layout: component --- Localization is handled by the browser's [`Intl.DateTimeFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat). No language packs are required. ```html:preview ``` ```jsx:react import SlFormatDate from '@shoelace-style/shoelace/dist/react/format-date'; const App = () => ; ``` The `date` attribute determines the date/time to use when formatting. It must be a string that [`Date.parse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse) can interpret or a [`Date`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date) object set via JavaScript. If omitted, the current date/time will be assumed. :::tip When using strings, avoid ambiguous dates such as `03/04/2020` which can be interpreted as March 4 or April 3 depending on the user's browser and locale. Instead, always use a valid [ISO 8601 date time string](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse#Date_Time_String_Format) to ensure the date will be parsed properly by all clients. ::: ## Examples ### Date & Time Formatting Formatting options are based on those found in the [`Intl.DateTimeFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat). When formatting options are provided, the date/time will be formatted according to those values. When no formatting options are provided, a localized, numeric date will be displayed instead. ```html:preview




      ``` ```jsx:react import SlFormatDate from '@shoelace-style/shoelace/dist/react/format-date'; const App = () => ( <> {/* Human-readable date */}
      {/* Time */}
      {/* Weekday */}
      {/* Month */}
      {/* Year */}
      {/* No formatting options */} ); ``` ### Hour Formatting By default, the browser will determine whether to use 12-hour or 24-hour time. To force one or the other, set the `hour-format` attribute to `12` or `24`. ```html:preview
      ``` ```jsx:react import SlFormatDate from '@shoelace-style/shoelace/dist/react/format-date'; const App = () => ( <>
      ); ``` ### Localization Use the `lang` attribute to set the date/time formatting locale. ```html:preview English:
      French:
      Russian: ``` ```jsx:react import SlFormatDate from '@shoelace-style/shoelace/dist/react/format-date'; const App = () => ( <> English:
      French:
      Russian: ); ``` ================================================ FILE: docs/pages/components/format-number.md ================================================ --- meta: title: Format Number description: Formats a number using the specified locale and options. layout: component --- Localization is handled by the browser's [`Intl.NumberFormat` API](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat). No language packs are required. ```html:preview


      ``` {% raw %} ```jsx:react import { useState } from 'react'; import SlFormatNumber from '@shoelace-style/shoelace/dist/react/format-number'; import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => { const [value, setValue] = useState(1000); return ( <>

      setValue(event.target.value)} /> ); }; ``` {% endraw %} ## Examples ### Percentages To get the value as a percent, set the `type` attribute to `percent`. ```html:preview



      ``` ```jsx:react import SlFormatNumber from '@shoelace-style/shoelace/dist/react/format-number'; const App = () => ( <>



      ); ``` ### Localization Use the `lang` attribute to set the number formatting locale. ```html:preview English:
      German:
      Russian: ``` ```jsx:react import SlFormatNumber from '@shoelace-style/shoelace/dist/react/format-number'; const App = () => ( <> English:
      German:
      Russian: ); ``` ### Currency To format a number as a monetary value, set the `type` attribute to `currency` and set the `currency` attribute to the desired ISO 4217 currency code. You should also specify `lang` to ensure the the number is formatted correctly for the target locale. ```html:preview



      ``` ```jsx:react import SlFormatNumber from '@shoelace-style/shoelace/dist/react/format-number'; const App = () => ( <>



      ); ``` ================================================ FILE: docs/pages/components/icon-button.md ================================================ --- meta: title: Icon Button description: Icons buttons are simple, icon-only buttons that can be used for actions and in toolbars. layout: component --- For a full list of icons that come bundled with Shoelace, refer to the [icon component](/components/icon). ```html:preview ``` ```jsx:react import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; const App = () => ; ``` ## Examples ### Sizes Icon buttons inherit their parent element's `font-size`. ```html:preview ``` {% raw %} ```jsx:react import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; const App = () => ( <> ); ``` {% endraw %} ### Colors Icon buttons are designed to have a uniform appearance, so their color is not inherited. However, you can still customize them by styling the `base` part. ```html:preview
      ``` ```jsx:react import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; const css = ` .icon-button-color sl-icon-button::part(base) { color: #b00091; } .icon-button-color sl-icon-button::part(base):hover, .icon-button-color sl-icon-button::part(base):focus { color: #c913aa; } .icon-button-color sl-icon-button::part(base):active { color: #960077; } `; const App = () => ( <>
      ); ``` ### Link Buttons Use the `href` attribute to convert the button to a link. ```html:preview ``` ```jsx:react import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; const App = () => ; ``` ### Icon Button with Tooltip Wrap a tooltip around an icon button to provide contextual information to the user. ```html:preview ``` ```jsx:react import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; import SlTooltip from '@shoelace-style/shoelace/dist/react/tooltip'; const App = () => ( ); ``` ### Disabled Use the `disabled` attribute to disable the icon button. ```html:preview ``` ```jsx:react import SlIconButton from '@shoelace-style/shoelace/dist/react/icon-button'; const App = () => ; ``` ================================================ FILE: docs/pages/components/icon.md ================================================ --- meta: title: Icon description: Icons are symbols that can be used to represent various options within an application. layout: component --- Shoelace comes bundled with over 1,500 icons courtesy of the [Bootstrap Icons](https://icons.getbootstrap.com/) project. These icons are part of the `default` icon library. If you prefer, you can register [custom icon libraries](#icon-libraries) as well. :::tip Depending on how you're loading Shoelace, you may need to copy icon assets and/or [set the base path](/getting-started/installation/#setting-the-base-path) so Shoelace knows where to load them from. Otherwise, icons may not appear and you'll see 404 Not Found errors in the dev console. ::: ## Default Icons All available icons in the `default` icon library are shown below. Click or tap on any icon to copy its name, then you can use it in your HTML like this. ```html ``` ## Examples ### Colors Icons inherit their color from the current text color. Thus, you can set the `color` property on the `` element or an ancestor to change the color. ```html:preview
      ``` {% raw %} ```jsx:react import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => ( <>
      ); ``` {% endraw %} ### Sizing Icons are sized relative to the current font size. To change their size, set the `font-size` property on the icon itself or on a parent element as shown below. ```html:preview
      ``` {% raw %} ```jsx:react import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => (
      ); ``` {% endraw %} ### Labels For non-decorative icons, use the `label` attribute to announce it to assistive devices. ```html:preview ``` ```jsx:react import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => ; ``` ### Custom Icons Custom icons can be loaded individually with the `src` attribute. Only SVGs on a local or CORS-enabled endpoint are supported. If you're using more than one custom icon, it might make sense to register a [custom icon library](#icon-libraries). ```html:preview ``` {% raw %} ```jsx:react import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; const App = () => ; ``` {% endraw %} ## Icon Libraries You can register additional icons to use with the `` component through icon libraries. Icon files can exist locally or on a CORS-enabled endpoint (e.g. a CDN). There is no limit to how many icon libraries you can register and there is no cost associated with registering them, as individual icons are only requested when they're used. Shoelace ships with two built-in icon libraries, `default` and `system`. The [default icon library](#customizing-the-default-library) contains all of the icons in the Bootstrap Icons project. The [system icon library](#customizing-the-system-library) contains only a small subset of icons that are used internally by Shoelace components. To register an additional icon library, use the `registerIconLibrary()` function that's exported from `utilities/icon-library.js`. At a minimum, you must provide a name and a resolver function. The resolver function translates an icon name to a URL where the corresponding SVG file exists. Refer to the examples below to better understand how it works. If necessary, a mutator function can be used to mutate the SVG element before rendering. This is necessary for some libraries due to the many possible ways SVGs are crafted. For example, icons should ideally inherit the current text color via `currentColor`, so you may need to apply `fill="currentColor` or `stroke="currentColor"` to the SVG element using this function. Here's an example that registers an icon library located in the `/assets/icons` directory. ```html ``` To display an icon, set the `library` and `name` attributes of an `` element. ```html ``` If an icon is used before registration occurs, it will be empty initially but shown when registered. The following examples demonstrate how to register a number of popular, open source icon libraries via CDN. Feel free to adapt the code as you see fit to use your own origin or naming conventions. ### Boxicons This will register the [Boxicons](https://boxicons.com/) library using the jsDelivr CDN. This library has three variations: regular (`bx-*`), solid (`bxs-*`), and logos (`bxl-*`). A mutator function is required to set the SVG's `fill` to `currentColor`. Icons in this library are licensed under the [Creative Commons 4.0 License](https://github.com/atisawd/boxicons#license). ```html:preview


      ``` ### Lucide This will register the [Lucide](https://lucide.dev/) icon library using the jsDelivr CDN. This project is a community-maintained fork of the popular [Feather](https://feathericons.com/) icon library. Icons in this library are licensed under the [MIT License](https://github.com/lucide-icons/lucide/blob/master/LICENSE). ```html:preview
      ``` ### Font Awesome This will register the [Font Awesome Free](https://fontawesome.com/) library using the jsDelivr CDN. This library has three variations: regular (`far-*`), solid (`fas-*`), and brands (`fab-*`). A mutator function is required to set the SVG's `fill` to `currentColor`. Icons in this library are licensed under the [Font Awesome Free License](https://github.com/FortAwesome/Font-Awesome/blob/master/LICENSE.txt). Some of the icons that appear on the Font Awesome website require a license and are therefore not available in the CDN. ```html:preview


      ``` ### Heroicons This will register the [Heroicons](https://heroicons.com/) library using the jsDelivr CDN. Icons in this library are licensed under the [MIT License](https://github.com/tailwindlabs/heroicons/blob/master/LICENSE). ```html:preview
      ``` ### Iconoir This will register the [Iconoir](https://iconoir.com/) library using the jsDelivr CDN. Icons in this library are licensed under the [MIT License](https://github.com/lucaburgio/iconoir/blob/master/LICENSE). ```html:preview
      ``` ### Ionicons This will register the [Ionicons](https://ionicons.com/) library using the jsDelivr CDN. This library has three variations: outline (default), filled (`*-filled`), and sharp (`*-sharp`). A mutator function is required to polyfill a handful of styles we're not including. Icons in this library are licensed under the [MIT License](https://github.com/ionic-team/ionicons/blob/master/LICENSE). ```html:preview


      ``` ### Jam Icons This will register the [Jam Icons](https://jam-icons.com/) library using the jsDelivr CDN. This library has two variations: regular (default) and filled (`*-f`). A mutator function is required to set the SVG's `fill` to `currentColor`. Icons in this library are licensed under the [MIT License](https://github.com/michaelampr/jam/blob/master/LICENSE). ```html:preview

      ``` ### Material Icons This will register the [Material Icons](https://material.io/resources/icons/?style=baseline) library using the jsDelivr CDN. This library has three variations: outline (default), round (`*_round`), and sharp (`*_sharp`). A mutator function is required to set the SVG's `fill` to `currentColor`. Icons in this library are licensed under the [Apache 2.0 License](https://github.com/google/material-design-icons/blob/master/LICENSE). ```html:preview


      ``` ### Remix Icon This will register the [Remix Icon](https://remixicon.com/) library using the jsDelivr CDN. This library groups icons by categories, so the name must include the category and icon separated by a slash, as well as the `-line` or `-fill` suffix as needed. A mutator function is required to set the SVG's `fill` to `currentColor`. Icons in this library are licensed under the [Apache 2.0 License](https://github.com/Remix-Design/RemixIcon/blob/master/License). ```html:preview

      ``` ### Tabler Icons This will register the [Tabler Icons](https://tabler-icons.io/) library using the jsDelivr CDN. This library features over 1,950 open source icons. Icons in this library are licensed under the [MIT License](https://github.com/tabler/tabler-icons/blob/master/LICENSE). ```html:preview

      ``` ### Unicons This will register the [Unicons](https://iconscout.com/unicons) library using the jsDelivr CDN. This library has two variations: line (default) and solid (`*-s`). A mutator function is required to set the SVG's `fill` to `currentColor`. Icons in this library are licensed under the [Apache 2.0 License](https://github.com/Iconscout/unicons/blob/master/LICENSE). Some of the icons that appear on the Unicons website, particularly many of the solid variations, require a license and are therefore not available in the CDN. ```html:preview

      ``` ### Customizing the Default Library The default icon library contains over 1,300 icons courtesy of the [Bootstrap Icons](https://icons.getbootstrap.com/) project. These are the icons that display when you use `` without the `library` attribute. If you prefer to have these icons resolve elsewhere or to a different icon library, register an icon library using the `default` name and a custom resolver. This example will load the same set of icons from the jsDelivr CDN instead of your local assets folder. ```html ``` #### Customize the default library to use SVG sprites To improve performance you can use a SVG sprites to avoid multiple trips for each SVG. The browser will load the sprite sheet once and then you reference the particular SVG within the sprite sheet using hash selector. As always, make sure to benchmark these changes. When using HTTP/2, it may in fact be more bandwidth-friendly to use multiple small requests instead of 1 large sprite sheet. :::danger When using sprite sheets, the `sl-load` and `sl-error` events will not fire. ::: :::danger For security reasons, browsers may apply the same-origin policy on `` elements located in the `` shadow DOM and may refuse to load a cross-origin URL. There is currently no defined way to set a cross-origin policy for `` elements. For this reason, sprite sheets should only be used if you're self-hosting them. ::: ```html:preview
      ``` ### Customizing the System Library The system library contains only the icons used internally by Shoelace components. Unlike the default icon library, the system library does not rely on physical assets. Instead, its icons are hard-coded as data URIs into the resolver to ensure their availability. If you want to change the icons Shoelace uses internally, you can register an icon library using the `system` name and a custom resolver. If you choose to do this, it's your responsibility to provide all of the icons that are required by components. You can reference `src/components/library.system.ts` for a complete list of system icons used by Shoelace. ```html ``` ================================================ FILE: docs/pages/components/image-comparer.md ================================================ --- meta: title: Image Comparer description: Compare visual differences between similar photos with a sliding panel. layout: component --- For best results, use images that share the same dimensions. The slider can be controlled by dragging or pressing the left and right arrow keys. (Tip: press shift + arrows to move the slider in larger intervals, or home + end to jump to the beginning or end.) ```html:preview Grayscale version of kittens in a basket looking around. Color version of kittens in a basket looking around. ``` ```jsx:react import SlImageComparer from '@shoelace-style/shoelace/dist/react/image-comparer'; const App = () => ( Grayscale version of kittens in a basket looking around. Color version of kittens in a basket looking around. ); ``` ## Examples ### Initial Position Use the `position` attribute to set the initial position of the slider. This is a percentage from `0` to `100`. ```html:preview A person sitting on bricks wearing untied boots. A person sitting on a yellow curb tying shoelaces on a boot. ``` ```jsx:react import SlImageComparer from '@shoelace-style/shoelace/dist/react/image-comparer'; const App = () => ( A person sitting on bricks wearing untied boots. A person sitting on a yellow curb tying shoelaces on a boot. ); ``` ================================================ FILE: docs/pages/components/include.md ================================================ --- meta: title: Include description: Includes give you the power to embed external HTML files into the page. layout: component --- Included files are asynchronously requested using `window.fetch()`. Requests are cached, so the same file can be included multiple times, but only one request will be made. The included content will be inserted into the `` element's default slot so it can be easily accessed and styled through the light DOM. ```html:preview ``` ```jsx:react import SlInclude from '@shoelace-style/shoelace/dist/react/include'; const App = () => ; ``` ## Examples ### Listening for Events When an include file loads successfully, the `sl-load` event will be emitted. You can listen for this event to add custom loading logic to your includes. If the request fails, the `sl-error` event will be emitted. In this case, `event.detail.status` will contain the resulting HTTP status code of the request, e.g. 404 (not found). ```html ``` ================================================ FILE: docs/pages/components/input.md ================================================ --- meta: title: Input description: Inputs collect data from the user. layout: component --- ```html:preview ``` ```jsx:react import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => ; ``` :::tip This component works with standard `` elements. Please refer to the section on [form controls](/getting-started/form-controls) to learn more about form submission and client-side validation. ::: ## Examples ### Labels Use the `label` attribute to give the input an accessible label. For labels that contain HTML, use the `label` slot instead. ```html:preview ``` ```jsx:react import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => ; ``` ### Help Text Add descriptive help text to an input with the `help-text` attribute. For help texts that contain HTML, use the `help-text` slot instead. ```html:preview ``` ```jsx:react import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => ; ``` ### Placeholders Use the `placeholder` attribute to add a placeholder. ```html:preview ``` ```jsx:react import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => ; ``` ### Clearable Add the `clearable` attribute to add a clear button when the input has content. ```html:preview ``` ```jsx:react import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => ; ``` ### Toggle Password Add the `password-toggle` attribute to add a toggle button that will show the password when activated. ```html:preview ``` ```jsx:react import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => ; ``` ### Filled Inputs Add the `filled` attribute to draw a filled input. ```html:preview ``` ```jsx:react import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => ; ``` ### Disabled Use the `disabled` attribute to disable an input. ```html:preview ``` ```jsx:react import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => ; ``` ### Sizes Use the `size` attribute to change an input's size. ```html:preview

      ``` ```jsx:react import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => ( <>

      ); ``` ### Pill Use the `pill` attribute to give inputs rounded edges. ```html:preview

      ``` ```jsx:react import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => ( <>

      ); ``` ### Input Types The `type` attribute controls the type of input the browser renders. ```html:preview

      ``` ```jsx:react import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => ( <>

      ); ``` ### Prefix & Suffix Icons Use the `prefix` and `suffix` slots to add icons. ```html:preview

      ``` ```jsx:react import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; import SlInput from '@shoelace-style/shoelace/dist/react/input'; const App = () => ( <>

      ); ``` ### Customizing Label Position Use [CSS parts](#css-parts) to customize the way form controls are drawn. This example uses CSS grid to position the label to the left of the control, but the possible orientations are nearly endless. The same technique works for inputs, textareas, radio groups, and similar form controls. ```html:preview ``` ================================================ FILE: docs/pages/components/menu-item.md ================================================ --- meta: title: Menu Item description: Menu items provide options for the user to pick from in a menu. layout: component --- ```html:preview Option 1 Option 2 Option 3 Checkbox Disabled Prefix Icon Suffix Icon ``` {% raw %} ```jsx:react import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Option 1 Option 2 Option 3 Checkbox Disabled Prefix Icon Suffix Icon ); ``` {% endraw %} ## Examples ### Prefix & Suffix Add content to the start and end of menu items using the `prefix` and `suffix` slots. ```html:preview Home Messages 12 Settings ``` {% raw %} ```jsx:react import SlBadge from '@shoelace-style/shoelace/dist/react/badge'; import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlIcon from '@shoelace-style/shoelace/dist/react/icon'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Home Messages 12 Settings ); ``` {% endraw %} ### Disabled Add the `disabled` attribute to disable the menu item so it cannot be selected. ```html:preview Option 1 Option 2 Option 3 ``` {% raw %} ```jsx:react import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Option 1 Option 2 Option 3 ); ``` {% endraw %} ### Loading Use the `loading` attribute to indicate that a menu item is busy. Like a disabled menu item, clicks will be suppressed until the loading state is removed. ```html:preview Option 1 Option 2 Option 3 ``` {% raw %} ```jsx:react import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Option 1 Option 2 Option 3 ); ``` {% endraw %} ### Checkbox Menu Items Set the `type` attribute to `checkbox` to create a menu item that will toggle on and off when selected. You can use the `checked` attribute to set the initial state. Checkbox menu items are visually indistinguishable from regular menu items. Their ability to be toggled is primarily inferred from context, much like you'd find in the menu of a native app. ```html:preview Autosave Check Spelling Word Wrap ``` {% raw %} ```jsx:react import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Autosave Check Spelling Word Wrap ); ``` {% endraw %} ### Value & Selection The `value` attribute can be used to assign a hidden value, such as a unique identifier, to a menu item. When an item is selected, the `sl-select` event will be emitted and a reference to the item will be available at `event.detail.item`. You can use this reference to access the selected item's value, its checked state, and more. ```html:preview Option 1 Option 2 Option 3 Checkbox 4 Checkbox 5 Checkbox 6 ``` {% raw %} ```jsx:react import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => { function handleSelect(event) { const item = event.detail.item; // Toggle checked state item.checked = !item.checked; // Log value console.log(`Selected value: ${item.value}`); } return ( Option 1 Option 2 Option 3 ); }; ``` {% endraw %} ================================================ FILE: docs/pages/components/menu-label.md ================================================ --- meta: title: Menu Label description: Menu labels are used to describe a group of menu items. layout: component --- ```html:preview Fruits Apple Banana Orange Vegetables Broccoli Carrot Zucchini ``` {% raw %} ```jsx:react import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuLabel from '@shoelace-style/shoelace/dist/react/menu-label'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Fruits Apple Banana Orange Vegetables Broccoli Carrot Zucchini ); ``` {% endraw %} ================================================ FILE: docs/pages/components/menu.md ================================================ --- meta: title: Menu description: Menus provide a list of options for the user to choose from. layout: component --- You can use [menu items](/components/menu-item), [menu labels](/components/menu-label), and [dividers](/components/divider) to compose a menu. Menus support keyboard interactions, including type-to-select an option. ```html:preview Undo Redo Cut Copy Paste Delete ``` {% raw %} ```jsx:react import SlDivider from '@shoelace-style/shoelace/dist/react/divider'; import SlMenu from '@shoelace-style/shoelace/dist/react/menu'; import SlMenuItem from '@shoelace-style/shoelace/dist/react/menu-item'; const App = () => ( Undo Redo Cut Copy Paste Delete ); ``` {% endraw %} :::tip Menus are intended for system menus (dropdown menus, select menus, context menus, etc.). They should not be mistaken for navigation menus which serve a different purpose and have a different semantic meaning. If you're building navigation, use `