diff --git a/packages/html/src/define/ui/slider.ts b/packages/html/src/define/ui/slider.ts
index d65ebe40..bdad7b75 100644
--- a/packages/html/src/define/ui/slider.ts
+++ b/packages/html/src/define/ui/slider.ts
@@ -1,5 +1,6 @@
import { SliderElement } from '../../ui/slider/slider-element';
import { SliderFillElement } from '../../ui/slider/slider-fill-element';
+import { SliderPreviewElement } from '../../ui/slider/slider-preview-element';
import { SliderThumbElement } from '../../ui/slider/slider-thumb-element';
import { SliderTrackElement } from '../../ui/slider/slider-track-element';
import { SliderValueElement } from '../../ui/slider/slider-value-element';
@@ -8,6 +9,7 @@ import { safeDefine } from '../safe-define';
// Parent slider first — sub-elements consume its context.
safeDefine(SliderElement);
safeDefine(SliderFillElement);
+safeDefine(SliderPreviewElement);
safeDefine(SliderThumbElement);
safeDefine(SliderTrackElement);
safeDefine(SliderValueElement);
diff --git a/packages/html/src/define/ui/time-slider.ts b/packages/html/src/define/ui/time-slider.ts
index fda8f5ac..7136c12d 100644
--- a/packages/html/src/define/ui/time-slider.ts
+++ b/packages/html/src/define/ui/time-slider.ts
@@ -1,5 +1,6 @@
import { SliderBufferElement } from '../../ui/slider/slider-buffer-element';
import { SliderFillElement } from '../../ui/slider/slider-fill-element';
+import { SliderPreviewElement } from '../../ui/slider/slider-preview-element';
import { SliderThumbElement } from '../../ui/slider/slider-thumb-element';
import { SliderThumbnailElement } from '../../ui/slider/slider-thumbnail-element';
import { SliderTrackElement } from '../../ui/slider/slider-track-element';
@@ -11,6 +12,7 @@ import { safeDefine } from '../safe-define';
safeDefine(TimeSliderElement);
safeDefine(SliderBufferElement);
safeDefine(SliderFillElement);
+safeDefine(SliderPreviewElement);
safeDefine(SliderThumbElement);
safeDefine(SliderThumbnailElement);
safeDefine(SliderTrackElement);
diff --git a/packages/html/src/define/ui/volume-slider.ts b/packages/html/src/define/ui/volume-slider.ts
index a607a9f5..f87cbfc7 100644
--- a/packages/html/src/define/ui/volume-slider.ts
+++ b/packages/html/src/define/ui/volume-slider.ts
@@ -1,4 +1,5 @@
import { SliderFillElement } from '../../ui/slider/slider-fill-element';
+import { SliderPreviewElement } from '../../ui/slider/slider-preview-element';
import { SliderThumbElement } from '../../ui/slider/slider-thumb-element';
import { SliderTrackElement } from '../../ui/slider/slider-track-element';
import { SliderValueElement } from '../../ui/slider/slider-value-element';
@@ -8,6 +9,7 @@ import { safeDefine } from '../safe-define';
// Parent slider first — sub-elements consume its context.
safeDefine(VolumeSliderElement);
safeDefine(SliderFillElement);
+safeDefine(SliderPreviewElement);
safeDefine(SliderThumbElement);
safeDefine(SliderTrackElement);
safeDefine(SliderValueElement);
diff --git a/packages/html/src/ui/slider/slider-preview-element.ts b/packages/html/src/ui/slider/slider-preview-element.ts
new file mode 100644
index 00000000..a3795fc4
--- /dev/null
+++ b/packages/html/src/ui/slider/slider-preview-element.ts
@@ -0,0 +1,56 @@
+import type { SliderPreviewOverflow } from '@videojs/core/dom';
+import { applyStateDataAttrs, getSliderPreviewStyle } from '@videojs/core/dom';
+import type { PropertyDeclarationMap, PropertyValues } from '@videojs/element';
+import { ContextConsumer } from '@videojs/element/context';
+import { applyStyles } from '@videojs/utils/dom';
+
+import { MediaElement } from '../media-element';
+import { sliderContext } from './context';
+
+export class SliderPreviewElement extends MediaElement {
+ static readonly tagName = 'media-slider-preview';
+
+ static override properties = {
+ overflow: { type: String },
+ } satisfies PropertyDeclarationMap<'overflow'>;
+
+ overflow: SliderPreviewOverflow = 'clamp';
+
+ readonly #ctx = new ContextConsumer(this, {
+ context: sliderContext,
+ subscribe: true,
+ });
+
+ #resizeObserver: ResizeObserver | null = null;
+ #width = 0;
+
+ override connectedCallback(): void {
+ super.connectedCallback();
+
+ this.#resizeObserver = new ResizeObserver(([entry]) => {
+ this.#width = entry!.contentRect.width;
+ this.#applyPosition();
+ });
+
+ this.#resizeObserver.observe(this);
+ }
+
+ override disconnectedCallback(): void {
+ super.disconnectedCallback();
+ this.#resizeObserver?.disconnect();
+ this.#resizeObserver = null;
+ }
+
+ #applyPosition(): void {
+ applyStyles(this, getSliderPreviewStyle(this.#width, this.overflow));
+ }
+
+ protected override update(_changed: PropertyValues): void {
+ super.update(_changed);
+
+ const ctx = this.#ctx.value;
+ if (ctx) applyStateDataAttrs(this, ctx.state, ctx.stateAttrMap);
+
+ this.#applyPosition();
+ }
+}
diff --git a/packages/html/src/ui/slider/tests/slider-preview-element.test.ts b/packages/html/src/ui/slider/tests/slider-preview-element.test.ts
new file mode 100644
index 00000000..ab030f70
--- /dev/null
+++ b/packages/html/src/ui/slider/tests/slider-preview-element.test.ts
@@ -0,0 +1,120 @@
+import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
+
+import { SliderElement } from '../slider-element';
+import { SliderPreviewElement } from '../slider-preview-element';
+
+// jsdom doesn't provide ResizeObserver.
+beforeAll(() => {
+ globalThis.ResizeObserver = class ResizeObserver {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+ } as unknown as typeof globalThis.ResizeObserver;
+});
+
+let tagCounter = 0;
+
+function uniqueTag(base: string): string {
+ return `${base}-${tagCounter++}`;
+}
+
+function createElement(Base: abstract new () => Element): Element {
+ const tag = uniqueTag('test-slp');
+ customElements.define(tag, class extends (Base as unknown as typeof HTMLElement) {});
+ return document.createElement(tag) as Element;
+}
+
+afterEach(() => {
+ document.body.innerHTML = '';
+});
+
+describe('SliderPreviewElement', () => {
+ it('has the correct tag name', () => {
+ expect(SliderPreviewElement.tagName).toBe('media-slider-preview');
+ });
+
+ it('defaults overflow to clamp', () => {
+ const el = createElement(SliderPreviewElement);
+ expect(el.overflow).toBe('clamp');
+ });
+
+ it('sets structural positioning styles after connect', async () => {
+ const slider = createElement(SliderElement);
+ const preview = createElement(SliderPreviewElement);
+
+ slider.appendChild(preview);
+ document.body.appendChild(slider);
+
+ await slider.updateComplete;
+ await preview.updateComplete;
+
+ expect(preview.style.position).toBe('absolute');
+ expect(preview.style.pointerEvents).toBe('none');
+ expect(preview.style.width).toBe('max-content');
+ });
+
+ it('applies clamped left style by default', async () => {
+ const slider = createElement(SliderElement);
+ const preview = createElement(SliderPreviewElement);
+
+ // jsdom rejects CSS min()/calc() — spy on setProperty to capture values.
+ const spy = vi.spyOn(preview.style, 'setProperty');
+
+ slider.appendChild(preview);
+ document.body.appendChild(slider);
+
+ await slider.updateComplete;
+ await preview.updateComplete;
+
+ const leftCall = spy.mock.calls.find(([key]) => key === 'left');
+ expect(leftCall).toBeTruthy();
+ expect(leftCall![1]).toContain('min(');
+ expect(leftCall![1]).toContain('max(');
+ });
+
+ it('applies unclamped left style when overflow is visible', async () => {
+ const slider = createElement(SliderElement);
+ const preview = createElement(SliderPreviewElement);
+ preview.overflow = 'visible';
+
+ const spy = vi.spyOn(preview.style, 'setProperty');
+
+ slider.appendChild(preview);
+ document.body.appendChild(slider);
+
+ await slider.updateComplete;
+ await preview.updateComplete;
+
+ const leftCall = spy.mock.calls.find(([key]) => key === 'left');
+ expect(leftCall).toBeTruthy();
+ expect(leftCall![1]).toContain('calc(var(--media-slider-pointer)');
+ expect(leftCall![1]).not.toContain('min(');
+ });
+
+ it('propagates data attributes from slider state', async () => {
+ const slider = createElement(SliderElement);
+ const preview = createElement(SliderPreviewElement);
+
+ slider.appendChild(preview);
+ document.body.appendChild(slider);
+
+ await slider.updateComplete;
+ await preview.updateComplete;
+
+ expect(preview.getAttribute('data-orientation')).toBe('horizontal');
+ });
+
+ it('cleans up ResizeObserver on disconnect', async () => {
+ const slider = createElement(SliderElement);
+ const preview = createElement(SliderPreviewElement);
+
+ slider.appendChild(preview);
+ document.body.appendChild(slider);
+
+ await slider.updateComplete;
+ await preview.updateComplete;
+
+ // Should not throw when removed.
+ slider.removeChild(preview);
+ });
+});