diff --git a/src/baklava.ts b/src/baklava.ts
index a3813b839..4226bbd49 100644
--- a/src/baklava.ts
+++ b/src/baklava.ts
@@ -25,6 +25,7 @@ export { default as BlRadioGroup } from "./components/radio-group/bl-radio-group
export { default as BlRadio } from "./components/radio-group/radio/bl-radio";
export { default as BlSelect } from "./components/select/bl-select";
export { default as BlSelectOption } from "./components/select/option/bl-select-option";
+export { default as BlSkeleton } from "./components/skeleton/bl-skeleton";
export { default as BlSpinner } from "./components/spinner/bl-spinner";
export { default as BlSplitButton } from "./components/split-button/bl-split-button";
export { default as BlStepper } from "./components/stepper/bl-stepper";
diff --git a/src/components/skeleton/bl-skeleton.css b/src/components/skeleton/bl-skeleton.css
new file mode 100644
index 000000000..a91ddb9e5
--- /dev/null
+++ b/src/components/skeleton/bl-skeleton.css
@@ -0,0 +1,58 @@
+:host {
+ display: block;
+}
+
+.skeleton {
+ --bg-color: var(--bl-skeleton-bg-color, var(--bl-color-neutral-lightest));
+ --highlight-color: var(--bl-skeleton-highlight-color, var(--bl-color-neutral-full));
+ --radius: var(--bl-skeleton-radius, var(--bl-border-radius-s));
+
+ background-color: var(--bg-color);
+ border-radius: var(--radius);
+ width: 100%;
+ height: var(--bl-size-m);
+ overflow: hidden;
+ position: relative;
+}
+
+:host([variant="circle"]) .skeleton {
+ --radius: var(--bl-border-radius-circle);
+
+ width: var(--bl-size-3xl);
+ height: var(--bl-size-3xl);
+}
+
+:host([variant="text"]) .skeleton {
+ height: var(--bl-size-xs);
+ border-radius: var(--bl-border-radius-xs);
+}
+
+:host([effect="pulse"]) .skeleton {
+ animation: pulse 1.5s ease-in-out infinite;
+}
+
+:host([effect="wave"]) .skeleton::after {
+ content: "";
+ position: absolute;
+ inset: 0;
+ transform: translateX(-100%);
+ background: linear-gradient(90deg, transparent, var(--highlight-color), transparent);
+ animation: wave 1.6s linear infinite;
+}
+
+@keyframes pulse {
+ 0%,
+ 100% {
+ opacity: 1;
+ }
+
+ 50% {
+ opacity: 0.4;
+ }
+}
+
+@keyframes wave {
+ 100% {
+ transform: translateX(100%);
+ }
+}
diff --git a/src/components/skeleton/bl-skeleton.stories.mdx b/src/components/skeleton/bl-skeleton.stories.mdx
new file mode 100644
index 000000000..e1291937f
--- /dev/null
+++ b/src/components/skeleton/bl-skeleton.stories.mdx
@@ -0,0 +1,172 @@
+import { ArgsTable, Canvas, Meta, Story } from '@storybook/addon-docs';
+import { html } from 'lit';
+import { ifDefined } from 'lit/directives/if-defined.js';
+import { styleMap } from 'lit/directives/style-map.js';
+
+
+
+export const SkeletonTemplate = (args) => html``;
+
+export const VariantTemplate = () => html`
+
+`;
+
+export const EffectTemplate = () => html`
+
+`;
+
+export const CustomSizeTemplate = () => html`
+
+
+
+
+
+
+`;
+
+export const CardTemplate = () => html`
+
+`;
+
+# Skeleton
+
+Skeleton component provides a placeholder preview of content before data is loaded. It reduces the perceived loading time and provides a better user experience.
+
+## Variants
+
+Skeleton has 3 variants: `rect` (default), `circle`, and `text`.
+
+- **rect** — General-purpose rectangular placeholder. Use for images, cards, and content blocks.
+- **circle** — Circular placeholder. Use for avatars and profile pictures.
+- **text** — Thin line placeholder. Use for text content and paragraphs.
+
+
+
+## Animation Effects
+
+Skeleton supports 3 animation effects: `pulse` (default), `wave`, and `none`.
+
+
+
+## Custom Sizes
+
+You can set custom `width` and `height` properties to control the skeleton dimensions.
+
+
+
+## Card Loading Example
+
+Combine multiple skeleton elements to create realistic loading placeholders for complex layouts.
+
+
+
+## Customizing Colors
+
+You can customize the skeleton appearance using CSS custom properties:
+
+```css
+bl-skeleton {
+ --bl-skeleton-bg-color: #e0e0e0;
+ --bl-skeleton-highlight-color: #f5f5f5;
+}
+```
+
+## Reference
+
+
diff --git a/src/components/skeleton/bl-skeleton.test.ts b/src/components/skeleton/bl-skeleton.test.ts
new file mode 100644
index 000000000..3f22e5307
--- /dev/null
+++ b/src/components/skeleton/bl-skeleton.test.ts
@@ -0,0 +1,116 @@
+import { assert, expect, fixture, html } from "@open-wc/testing";
+import BlSkeleton from "./bl-skeleton";
+
+import type typeOfBlSkeleton from "./bl-skeleton";
+
+describe("bl-skeleton", () => {
+ it("is defined", () => {
+ const el = document.createElement("bl-skeleton");
+
+ assert.instanceOf(el, BlSkeleton);
+ });
+
+ it("renders with default values", async () => {
+ const el = await fixture(html``);
+
+ assert.shadowDom.equal(
+ el,
+ ""
+ );
+ });
+
+ it("has correct default property values", async () => {
+ const el = await fixture(html``);
+
+ expect(el.variant).to.equal("rect");
+ expect(el.effect).to.equal("pulse");
+ expect(el.width).to.be.undefined;
+ expect(el.height).to.be.undefined;
+ });
+
+ it("reflects variant attribute", async () => {
+ const el = await fixture(
+ html``
+ );
+
+ expect(el.variant).to.equal("circle");
+ expect(el.getAttribute("variant")).to.equal("circle");
+ });
+
+ it("reflects effect attribute", async () => {
+ const el = await fixture(
+ html``
+ );
+
+ expect(el.effect).to.equal("wave");
+ expect(el.getAttribute("effect")).to.equal("wave");
+ });
+
+ it("applies custom width via inline style", async () => {
+ const el = await fixture(
+ html``
+ );
+ const skeleton = el.shadowRoot!.querySelector(".skeleton")!;
+
+ expect(skeleton.style.width).to.equal("200px");
+ });
+
+ it("applies custom height via inline style", async () => {
+ const el = await fixture(
+ html``
+ );
+ const skeleton = el.shadowRoot!.querySelector(".skeleton")!;
+
+ expect(skeleton.style.height).to.equal("50px");
+ });
+
+ it("applies both width and height", async () => {
+ const el = await fixture(
+ html``
+ );
+ const skeleton = el.shadowRoot!.querySelector(".skeleton")!;
+
+ expect(skeleton.style.width).to.equal("300px");
+ expect(skeleton.style.height).to.equal("100px");
+ });
+
+ it("does not set inline width/height when not provided", async () => {
+ const el = await fixture(html``);
+ const skeleton = el.shadowRoot!.querySelector(".skeleton")!;
+
+ expect(skeleton.style.width).to.equal("");
+ expect(skeleton.style.height).to.equal("");
+ });
+
+ it("sets role=presentation and aria-hidden=true for accessibility", async () => {
+ const el = await fixture(html``);
+ const skeleton = el.shadowRoot!.querySelector(".skeleton")!;
+
+ expect(skeleton.getAttribute("role")).to.equal("presentation");
+ expect(skeleton.getAttribute("aria-hidden")).to.equal("true");
+ });
+
+ it("supports text variant", async () => {
+ const el = await fixture(
+ html``
+ );
+
+ expect(el.variant).to.equal("text");
+ expect(el.getAttribute("variant")).to.equal("text");
+ });
+
+ it("supports none effect", async () => {
+ const el = await fixture(
+ html``
+ );
+
+ expect(el.effect).to.equal("none");
+ expect(el.getAttribute("effect")).to.equal("none");
+ });
+
+ it("renders as block-level element by default", async () => {
+ const el = await fixture(html``);
+
+ expect(getComputedStyle(el).display).to.equal("block");
+ });
+});
diff --git a/src/components/skeleton/bl-skeleton.ts b/src/components/skeleton/bl-skeleton.ts
new file mode 100644
index 000000000..2f8731316
--- /dev/null
+++ b/src/components/skeleton/bl-skeleton.ts
@@ -0,0 +1,68 @@
+import { CSSResultGroup, html, LitElement, TemplateResult } from "lit";
+import { customElement, property } from "lit/decorators.js";
+import { styleMap } from "lit/directives/style-map.js";
+import style from "./bl-skeleton.css";
+
+export type SkeletonVariant = "rect" | "circle" | "text";
+export type SkeletonEffect = "pulse" | "wave" | "none";
+
+export const blSkeletonTag = "bl-skeleton";
+
+/**
+ * @tag bl-skeleton
+ * @summary Baklava Skeleton component
+ *
+ * @cssproperty [--bl-skeleton-bg-color=--bl-color-neutral-lightest] Sets the background color of skeleton
+ * @cssproperty [--bl-skeleton-highlight-color=--bl-color-neutral-full] Sets the highlight color for wave animation
+ * @cssproperty [--bl-skeleton-radius=--bl-border-radius-s] Overrides the border radius of skeleton
+ */
+@customElement(blSkeletonTag)
+export default class BlSkeleton extends LitElement {
+ static get styles(): CSSResultGroup {
+ return [style];
+ }
+
+ /**
+ * Sets the skeleton variant
+ */
+ @property({ type: String, reflect: true })
+ variant: SkeletonVariant = "rect";
+
+ /**
+ * Sets the animation effect
+ */
+ @property({ type: String, reflect: true })
+ effect: SkeletonEffect = "pulse";
+
+ /**
+ * Sets a custom width (any CSS value)
+ */
+ @property({ type: String })
+ width?: string;
+
+ /**
+ * Sets a custom height (any CSS value)
+ */
+ @property({ type: String })
+ height?: string;
+
+ render(): TemplateResult {
+ const inlineStyles: Record = {};
+
+ if (this.width) inlineStyles.width = this.width;
+ if (this.height) inlineStyles.height = this.height;
+
+ return html``;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ [blSkeletonTag]: BlSkeleton;
+ }
+}
diff --git a/src/components/skeleton/doc/ADR.md b/src/components/skeleton/doc/ADR.md
new file mode 100644
index 000000000..c4135479f
--- /dev/null
+++ b/src/components/skeleton/doc/ADR.md
@@ -0,0 +1,92 @@
+## Figma Design Document
+
+_TBD_
+
+## Implementation
+
+Skeleton component provides placeholder loading states for content. It helps reduce perceived loading time by showing the structure of the page before the actual content is loaded.
+
+General usage example:
+
+```html
+
+```
+
+### Rules
+
+* Default variant is `rect` and default effect is `pulse`.
+* The `circle` variant uses equal width and height by default (`40px`). Override with `width` and `height` for custom sizes.
+* The `text` variant renders a thin line suitable for paragraph placeholders.
+* Custom `width` and `height` accept any valid CSS value (e.g. `200px`, `50%`, `10rem`).
+* Skeleton elements have `role="presentation"` and `aria-hidden="true"` to be hidden from assistive technologies.
+
+### Usage Examples
+
+Basic rectangular skeleton:
+
+```html
+
+```
+
+Circle skeleton for avatar placeholders:
+
+```html
+
+```
+
+Text skeleton for paragraph placeholders:
+
+```html
+
+
+
+```
+
+Wave animation effect:
+
+```html
+
+```
+
+Card loading placeholder:
+
+```html
+
+
+
+
+
+
+```
+
+Custom colors:
+
+```css
+.custom-skeleton {
+ --bl-skeleton-bg-color: #e0e0e0;
+ --bl-skeleton-highlight-color: #f5f5f5;
+}
+```
+
+```html
+
+```
+
+## API Reference
+
+### Attributes
+
+| Attribute | Type | Description | Default Value |
+| --------- | ---- | ----------- | ------------- |
+| `variant` | `"rect"` \| `"circle"` \| `"text"` | Shape variant of the skeleton | `"rect"` |
+| `effect` | `"pulse"` \| `"wave"` \| `"none"` | Animation effect | `"pulse"` |
+| `width` | `string` | Custom CSS width | - |
+| `height` | `string` | Custom CSS height | - |
+
+### CSS Custom Properties
+
+| Property | Description | Default Value |
+| -------- | ----------- | ------------- |
+| `--bl-skeleton-bg-color` | Background color | `--bl-color-neutral-lightest` |
+| `--bl-skeleton-highlight-color` | Highlight color for wave animation | `--bl-color-neutral-full` |
+| `--bl-skeleton-radius` | Border radius override | `--bl-border-radius-s` |