docs(site): add Slider and Tooltip API reference pages (#862)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Darius Cepulis
2026-03-16 14:05:33 -05:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 487be3ea96
commit 7a5ce94bca
44 changed files with 1126 additions and 48 deletions
@@ -19,9 +19,10 @@ import InlineMarkdown from './InlineMarkdown.astro';
interface Props {
component: string;
partOrder?: string[];
}
const { component } = Astro.props;
const { component, partOrder } = Astro.props;
const { framework } = Astro.params;
if (!framework || !isValidFramework(framework)) {
throw new Error(`Invalid or missing framework param.`);
@@ -31,7 +32,7 @@ const entry = await getEntry('componentReference', kebabCase(component));
const apiRef: ComponentReference | null = entry?.data ?? null;
if (!apiRef) return;
const apiReferenceModel = createComponentReferenceModel(component, apiRef);
const apiReferenceModel = createComponentReferenceModel(component, apiRef, partOrder);
if (!apiReferenceModel) return;
const showAttributeName = framework === 'html';
@@ -0,0 +1,10 @@
---
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
import html from './BasicUsage.html?raw';
import './BasicUsage.css';
---
<HtmlDemo html={html} />
<script>
import "./BasicUsage.ts";
</script>
@@ -0,0 +1,59 @@
.html-slider-basic {
display: flex;
align-items: center;
padding: 24px;
background: #1a1a1a;
}
.html-slider-basic__slider {
position: relative;
width: 100%;
display: flex;
align-items: center;
height: 20px;
cursor: pointer;
}
.html-slider-basic__track {
position: absolute;
left: 0;
right: 0;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 9999px;
transition: height 150ms ease;
}
.html-slider-basic__slider[data-interactive] .html-slider-basic__track {
height: 6px;
}
.html-slider-basic__fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: var(--media-slider-fill);
background: white;
border-radius: 9999px;
}
.html-slider-basic__thumb {
position: absolute;
left: var(--media-slider-fill);
width: 14px;
height: 14px;
background: white;
border-radius: 50%;
transform: translateX(-50%) scale(0);
transition: transform 150ms ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.html-slider-basic__slider[data-interactive] .html-slider-basic__thumb {
transform: translateX(-50%) scale(1);
}
.html-slider-basic__slider[data-dragging] .html-slider-basic__thumb {
transform: translateX(-50%) scale(1.1);
}
@@ -0,0 +1,8 @@
<div class="html-slider-basic">
<media-slider class="html-slider-basic__slider" value="50">
<media-slider-track class="html-slider-basic__track">
<media-slider-fill class="html-slider-basic__fill"></media-slider-fill>
</media-slider-track>
<media-slider-thumb class="html-slider-basic__thumb"></media-slider-thumb>
</media-slider>
</div>
@@ -0,0 +1 @@
import '@videojs/html/ui/slider';
@@ -0,0 +1,10 @@
---
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
import html from './WithPreview.html?raw';
import './WithPreview.css';
---
<HtmlDemo html={html} />
<script>
import "./WithPreview.ts";
</script>
@@ -0,0 +1,81 @@
.html-slider-preview {
display: flex;
align-items: center;
padding: 40px 24px;
background: #1a1a1a;
}
.html-slider-preview__slider {
position: relative;
width: 100%;
display: flex;
align-items: center;
height: 20px;
cursor: pointer;
}
.html-slider-preview__track {
position: absolute;
left: 0;
right: 0;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 9999px;
transition: height 150ms ease;
}
.html-slider-preview__slider[data-interactive] .html-slider-preview__track {
height: 6px;
}
.html-slider-preview__fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: var(--media-slider-fill);
background: white;
border-radius: 9999px;
}
.html-slider-preview__thumb {
position: absolute;
left: var(--media-slider-fill);
width: 14px;
height: 14px;
background: white;
border-radius: 50%;
transform: translateX(-50%) scale(0);
transition: transform 150ms ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.html-slider-preview__slider[data-interactive] .html-slider-preview__thumb {
transform: translateX(-50%) scale(1);
}
.html-slider-preview__slider[data-dragging] .html-slider-preview__thumb {
transform: translateX(-50%) scale(1.1);
}
.html-slider-preview__preview {
position: absolute;
bottom: 100%;
margin-bottom: 6px;
pointer-events: none;
opacity: 0;
transition: opacity 150ms ease;
}
.html-slider-preview__slider[data-pointing] .html-slider-preview__preview {
opacity: 1;
}
.html-slider-preview__value {
background: rgba(0, 0, 0, 0.8);
color: white;
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
white-space: nowrap;
}
@@ -0,0 +1,11 @@
<div class="html-slider-preview">
<media-slider class="html-slider-preview__slider" value="50">
<media-slider-track class="html-slider-preview__track">
<media-slider-fill class="html-slider-preview__fill"></media-slider-fill>
</media-slider-track>
<media-slider-thumb class="html-slider-preview__thumb"></media-slider-thumb>
<media-slider-preview class="html-slider-preview__preview">
<media-slider-value type="pointer" class="html-slider-preview__value"></media-slider-value>
</media-slider-preview>
</media-slider>
</div>
@@ -0,0 +1 @@
import '@videojs/html/ui/slider';
@@ -0,0 +1,59 @@
.react-slider-basic {
display: flex;
align-items: center;
padding: 24px;
background: #1a1a1a;
}
.react-slider-basic__slider {
position: relative;
width: 100%;
display: flex;
align-items: center;
height: 20px;
cursor: pointer;
}
.react-slider-basic__track {
position: absolute;
left: 0;
right: 0;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 9999px;
transition: height 150ms ease;
}
.react-slider-basic__slider[data-interactive] .react-slider-basic__track {
height: 6px;
}
.react-slider-basic__fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: var(--media-slider-fill);
background: white;
border-radius: 9999px;
}
.react-slider-basic__thumb {
position: absolute;
left: var(--media-slider-fill);
width: 14px;
height: 14px;
background: white;
border-radius: 50%;
transform: translateX(-50%) scale(0);
transition: transform 150ms ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.react-slider-basic__slider[data-interactive] .react-slider-basic__thumb {
transform: translateX(-50%) scale(1);
}
.react-slider-basic__slider[data-dragging] .react-slider-basic__thumb {
transform: translateX(-50%) scale(1.1);
}
@@ -0,0 +1,19 @@
import { Slider } from '@videojs/react';
import { useState } from 'react';
import './BasicUsage.css';
export default function BasicUsage() {
const [value, setValue] = useState(50);
return (
<div className="react-slider-basic">
<Slider.Root className="react-slider-basic__slider" value={value} onValueChange={setValue}>
<Slider.Track className="react-slider-basic__track">
<Slider.Fill className="react-slider-basic__fill" />
</Slider.Track>
<Slider.Thumb className="react-slider-basic__thumb" />
</Slider.Root>
</div>
);
}
@@ -0,0 +1,81 @@
.react-slider-preview {
display: flex;
align-items: center;
padding: 40px 24px;
background: #1a1a1a;
}
.react-slider-preview__slider {
position: relative;
width: 100%;
display: flex;
align-items: center;
height: 20px;
cursor: pointer;
}
.react-slider-preview__track {
position: absolute;
left: 0;
right: 0;
height: 4px;
background: rgba(255, 255, 255, 0.3);
border-radius: 9999px;
transition: height 150ms ease;
}
.react-slider-preview__slider[data-interactive] .react-slider-preview__track {
height: 6px;
}
.react-slider-preview__fill {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: var(--media-slider-fill);
background: white;
border-radius: 9999px;
}
.react-slider-preview__thumb {
position: absolute;
left: var(--media-slider-fill);
width: 14px;
height: 14px;
background: white;
border-radius: 50%;
transform: translateX(-50%) scale(0);
transition: transform 150ms ease;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.4);
}
.react-slider-preview__slider[data-interactive] .react-slider-preview__thumb {
transform: translateX(-50%) scale(1);
}
.react-slider-preview__slider[data-dragging] .react-slider-preview__thumb {
transform: translateX(-50%) scale(1.1);
}
.react-slider-preview__preview {
position: absolute;
bottom: 100%;
margin-bottom: 6px;
pointer-events: none;
opacity: 0;
transition: opacity 150ms ease;
}
.react-slider-preview__slider[data-pointing] .react-slider-preview__preview {
opacity: 1;
}
.react-slider-preview__value {
background: rgba(0, 0, 0, 0.8);
color: white;
font-size: 12px;
padding: 2px 6px;
border-radius: 4px;
white-space: nowrap;
}
@@ -0,0 +1,22 @@
import { Slider } from '@videojs/react';
import { useState } from 'react';
import './WithPreview.css';
export default function WithPreview() {
const [value, setValue] = useState(50);
return (
<div className="react-slider-preview">
<Slider.Root className="react-slider-preview__slider" value={value} onValueChange={setValue}>
<Slider.Track className="react-slider-preview__track">
<Slider.Fill className="react-slider-preview__fill" />
</Slider.Track>
<Slider.Thumb className="react-slider-preview__thumb" />
<Slider.Preview className="react-slider-preview__preview">
<Slider.Value type="pointer" className="react-slider-preview__value" />
</Slider.Preview>
</Slider.Root>
</div>
);
}
@@ -0,0 +1,10 @@
---
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
import html from './BasicUsage.html?raw';
import './BasicUsage.css';
---
<HtmlDemo html={html} />
<script>
import "./BasicUsage.ts";
</script>
@@ -0,0 +1,28 @@
.html-tooltip-basic {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
}
.html-tooltip-basic__trigger {
padding: 6px 16px;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
color: black;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
cursor: pointer;
}
.html-tooltip-basic__popup {
--media-tooltip-side-offset: 8px;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
color: white;
border-radius: 6px;
padding: 4px 10px;
font-size: 13px;
white-space: nowrap;
pointer-events: none;
}
@@ -0,0 +1,6 @@
<div class="html-tooltip-basic">
<button type="button" commandfor="tooltip-demo" class="html-tooltip-basic__trigger">Hover me</button>
<media-tooltip id="tooltip-demo" class="html-tooltip-basic__popup">
Tooltip content
</media-tooltip>
</div>
@@ -0,0 +1 @@
import '@videojs/html/ui/tooltip';
@@ -0,0 +1,10 @@
---
import HtmlDemo from '@/components/docs/demos/HtmlDemo.astro';
import html from './Grouping.html?raw';
import './Grouping.css';
---
<HtmlDemo html={html} />
<script>
import "./Grouping.ts";
</script>
@@ -0,0 +1,27 @@
.html-tooltip-grouping {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
}
.html-tooltip-grouping__trigger {
padding: 6px 16px;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
color: black;
border: 1px solid rgba(255, 255, 255, 0.3);
cursor: pointer;
}
.html-tooltip-grouping__popup {
--media-tooltip-side-offset: 8px;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
color: white;
border-radius: 6px;
padding: 4px 10px;
font-size: 13px;
white-space: nowrap;
pointer-events: none;
}
@@ -0,0 +1,10 @@
<media-tooltip-group class="html-tooltip-grouping">
<button type="button" commandfor="tooltip-play" class="html-tooltip-grouping__trigger">Play</button>
<media-tooltip id="tooltip-play" class="html-tooltip-grouping__popup">Play video</media-tooltip>
<button type="button" commandfor="tooltip-mute" class="html-tooltip-grouping__trigger">Mute</button>
<media-tooltip id="tooltip-mute" class="html-tooltip-grouping__popup">Mute audio</media-tooltip>
<button type="button" commandfor="tooltip-fullscreen" class="html-tooltip-grouping__trigger">Fullscreen</button>
<media-tooltip id="tooltip-fullscreen" class="html-tooltip-grouping__popup">Enter fullscreen</media-tooltip>
</media-tooltip-group>
@@ -0,0 +1,2 @@
import '@videojs/html/ui/tooltip';
import '@videojs/html/ui/tooltip-group';
@@ -0,0 +1,34 @@
.react-tooltip-basic {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
}
.react-tooltip-basic__trigger {
padding: 6px 16px;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
color: black;
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
cursor: pointer;
}
.react-tooltip-basic__popup {
--media-tooltip-side-offset: 8px;
margin: 0;
border: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
color: white;
border-radius: 6px;
padding: 4px 10px;
font-size: 13px;
white-space: nowrap;
pointer-events: none;
}
.react-tooltip-basic__arrow {
fill: rgba(0, 0, 0, 0.85);
}
@@ -0,0 +1,17 @@
import { Tooltip } from '@videojs/react';
import './BasicUsage.css';
export default function BasicUsage() {
return (
<div className="react-tooltip-basic">
<Tooltip.Root>
<Tooltip.Trigger className="react-tooltip-basic__trigger">Hover me</Tooltip.Trigger>
<Tooltip.Popup className="react-tooltip-basic__popup">
<Tooltip.Arrow className="react-tooltip-basic__arrow" />
Tooltip content
</Tooltip.Popup>
</Tooltip.Root>
</div>
);
}
@@ -0,0 +1,33 @@
.react-tooltip-grouping {
display: flex;
align-items: center;
justify-content: center;
padding: 40px 24px;
}
.react-tooltip-grouping__trigger {
padding: 6px 16px;
background: rgba(255, 255, 255, 0.7);
backdrop-filter: blur(10px);
color: black;
border: 1px solid rgba(255, 255, 255, 0.3);
cursor: pointer;
}
.react-tooltip-grouping__popup {
--media-tooltip-side-offset: 8px;
margin: 0;
border: 0;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(10px);
color: white;
border-radius: 6px;
padding: 4px 10px;
font-size: 13px;
white-space: nowrap;
pointer-events: none;
}
.react-tooltip-grouping__arrow {
fill: rgba(0, 0, 0, 0.85);
}
@@ -0,0 +1,33 @@
import { Tooltip } from '@videojs/react';
import './Grouping.css';
export default function Grouping() {
return (
<div className="react-tooltip-grouping">
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger className="react-tooltip-grouping__trigger">Play</Tooltip.Trigger>
<Tooltip.Popup className="react-tooltip-grouping__popup">
<Tooltip.Arrow className="react-tooltip-grouping__arrow" />
Play video
</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger className="react-tooltip-grouping__trigger">Mute</Tooltip.Trigger>
<Tooltip.Popup className="react-tooltip-grouping__popup">
<Tooltip.Arrow className="react-tooltip-grouping__arrow" />
Mute audio
</Tooltip.Popup>
</Tooltip.Root>
<Tooltip.Root>
<Tooltip.Trigger className="react-tooltip-grouping__trigger">Fullscreen</Tooltip.Trigger>
<Tooltip.Popup className="react-tooltip-grouping__popup">
<Tooltip.Arrow className="react-tooltip-grouping__arrow" />
Enter fullscreen
</Tooltip.Popup>
</Tooltip.Root>
</Tooltip.Provider>
</div>
);
}
+1 -1
View File
@@ -73,7 +73,7 @@ media-controls:not([data-visible]) {
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
React renders `<div>` elements with the same data attributes. Add a `className` and use it as the selector:
React renders `<div>` elements. Add a `className` to style them:
```css
/* Click-through: clicks pass through controls to video beneath */
@@ -53,7 +53,7 @@ media-fullscreen-button[data-fullscreen] {
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
React renders a `<button>` with the same data attributes. Add a `className` and use it as the selector:
React renders a `<button>` element. Add a `className` to style it:
```css
/* In fullscreen */
@@ -62,7 +62,7 @@ media-mute-button:not([data-muted]) .icon-unmuted { display: inline; }
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
React renders a `<button>` with the same data attributes. Add a `className` and use it as the selector:
React renders a `<button>` element. Add a `className` to style it:
```css
.mute-button[data-muted] .icon-muted { display: inline; }
@@ -53,7 +53,7 @@ media-pip-button[data-pip] {
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
React renders a `<button>` with the same data attributes. Add a `className` and use it as the selector:
React renders a `<button>` element. Add a `className` to style it:
```css
/* In PiP mode */
@@ -57,7 +57,7 @@ media-play-button[data-ended] .replay-icon { display: inline; }
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
React renders a `<button>` with the same data attributes. Add a `className` and use it as the selector:
React renders a `<button>` element. Add a `className` to style it:
```css
/* Paused (but not ended) */
+1 -1
View File
@@ -78,7 +78,7 @@ media-popover {
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
React renders standard DOM elements with the same data attributes and CSS custom properties. Add a `className` and use it as the selector:
React renders standard DOM elements. Add a `className` to style them:
```css
.popover {
+1 -1
View File
@@ -56,7 +56,7 @@ media-poster:not([data-visible]) {
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
React renders an `<img>` with the same data attributes. Add a `className` and use it as the selector:
React renders an `<img>` element. Add a `className` to style it:
```css
.poster:not([data-visible]) {
+198
View File
@@ -0,0 +1,198 @@
---
title: Slider
frameworkTitle:
html: media-slider
description: A composable slider component with track, fill, thumb, preview, and value parts
---
import ComponentReference from "@/components/docs/api-reference/ComponentReference.astro";
import DocsLink from "@/components/docs/DocsLink.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
import StyleCase from "@/components/docs/StyleCase.astro";
import Demo from "@/components/docs/demos/Demo.astro";
{/* React demos */}
import BasicUsageDemoReact from "@/components/docs/demos/slider/react/css/BasicUsage";
import basicUsageReactTsx from "@/components/docs/demos/slider/react/css/BasicUsage.tsx?raw";
import basicUsageReactCss from "@/components/docs/demos/slider/react/css/BasicUsage.css?raw";
import WithPreviewDemoReact from "@/components/docs/demos/slider/react/css/WithPreview";
import withPreviewReactTsx from "@/components/docs/demos/slider/react/css/WithPreview.tsx?raw";
import withPreviewReactCss from "@/components/docs/demos/slider/react/css/WithPreview.css?raw";
{/* HTML demos */}
import BasicUsageDemoHtml from "@/components/docs/demos/slider/html/css/BasicUsage.astro";
import basicUsageHtml from "@/components/docs/demos/slider/html/css/BasicUsage.html?raw";
import basicUsageHtmlCss from "@/components/docs/demos/slider/html/css/BasicUsage.css?raw";
import basicUsageHtmlTs from "@/components/docs/demos/slider/html/css/BasicUsage.ts?raw";
import WithPreviewDemoHtml from "@/components/docs/demos/slider/html/css/WithPreview.astro";
import withPreviewHtml from "@/components/docs/demos/slider/html/css/WithPreview.html?raw";
import withPreviewHtmlCss from "@/components/docs/demos/slider/html/css/WithPreview.css?raw";
import withPreviewHtmlTs from "@/components/docs/demos/slider/html/css/WithPreview.ts?raw";
## Anatomy
<FrameworkCase frameworks={["react"]}>
```tsx
<Slider.Root>
<Slider.Track>
<Slider.Fill />
</Slider.Track>
<Slider.Thumb />
<Slider.Preview>
<Slider.Value type="pointer" />
</Slider.Preview>
</Slider.Root>
```
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
```html
<media-slider>
<media-slider-track>
<media-slider-fill></media-slider-fill>
</media-slider-track>
<media-slider-thumb></media-slider-thumb>
<media-slider-preview>
<media-slider-value type="pointer"></media-slider-value>
</media-slider-preview>
</media-slider>
```
</FrameworkCase>
## Behavior
The base Slider provides a generic range input. It manages value, pointer tracking, and drag interactions. Domain-specific sliders like <DocsLink slug="reference/time-slider">TimeSlider</DocsLink> and <DocsLink slug="reference/volume-slider">VolumeSlider</DocsLink> extend this with media-specific bindings.
The slider supports vertical orientation via the `orientation` prop (defaults to `"horizontal"`).
## Styling
Use [CSS custom properties](#root-css-custom-properties) to position fill, thumb, and preview elements:
<FrameworkCase frameworks={["html"]}>
```css
media-slider-fill {
width: var(--media-slider-fill);
}
media-slider-thumb {
left: var(--media-slider-fill);
}
```
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
React renders standard DOM elements. Add a `className` to style them:
```css
.slider-fill {
width: var(--media-slider-fill);
}
.slider-thumb {
left: var(--media-slider-fill);
}
```
</FrameworkCase>
Style based on [interaction state](#root-data-attributes):
<FrameworkCase frameworks={["html"]}>
```css
media-slider[data-interactive] media-slider-track {
height: 6px;
}
media-slider[data-pointing] media-slider-preview {
opacity: 1;
}
```
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
```css
.slider[data-interactive] .slider-track {
height: 6px;
}
.slider[data-pointing] .slider-preview {
opacity: 1;
}
```
</FrameworkCase>
## Accessibility
Renders with `role="slider"` and automatic ARIA attributes (`aria-valuemin`, `aria-valuemax`, `aria-valuenow`, `aria-valuetext`). Override the label with the `label` prop. Keyboard controls:
- <kbd>Arrow Left</kbd> / <kbd>Arrow Right</kbd>: step by `step` increment
- <kbd>Page Up</kbd> / <kbd>Page Down</kbd>: step by `largeStep` increment
- <kbd>Home</kbd>: jump to minimum
- <kbd>End</kbd>: jump to maximum
## Examples
### Basic
A slider with track, fill, and thumb.
<FrameworkCase frameworks={["react"]}>
<StyleCase styles={["css"]}>
<Demo
files={[
{ title: "App.tsx", code: basicUsageReactTsx, lang: "tsx" },
{ title: "App.css", code: basicUsageReactCss, lang: "css" },
]}
>
<BasicUsageDemoReact client:idle />
</Demo>
</StyleCase>
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
<StyleCase styles={["css"]}>
<Demo
files={[
{ title: "index.html", code: basicUsageHtml, lang: "html" },
{ title: "index.css", code: basicUsageHtmlCss, lang: "css" },
{ title: "index.ts", code: basicUsageHtmlTs, lang: "ts" },
]}
>
<BasicUsageDemoHtml />
</Demo>
</StyleCase>
</FrameworkCase>
### With Preview
A slider with a pointer-tracking preview that displays the value at the current pointer position.
<FrameworkCase frameworks={["react"]}>
<StyleCase styles={["css"]}>
<Demo
files={[
{ title: "App.tsx", code: withPreviewReactTsx, lang: "tsx" },
{ title: "App.css", code: withPreviewReactCss, lang: "css" },
]}
>
<WithPreviewDemoReact client:idle />
</Demo>
</StyleCase>
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
<StyleCase styles={["css"]}>
<Demo
files={[
{ title: "index.html", code: withPreviewHtml, lang: "html" },
{ title: "index.css", code: withPreviewHtmlCss, lang: "css" },
{ title: "index.ts", code: withPreviewHtmlTs, lang: "ts" },
]}
>
<WithPreviewDemoHtml />
</Demo>
</StyleCase>
</FrameworkCase>
<ComponentReference component="Slider" />
@@ -116,7 +116,7 @@ media-thumbnail[data-error] {
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
React renders a `<div>` with the same data attributes. Add a `className` and use it as the selector:
React renders a `<div>` element. Add a `className` to style it:
```css
.thumbnail[data-hidden] {
@@ -48,17 +48,17 @@ Use [CSS custom properties](#root-css-custom-properties) to style the fill, poin
<FrameworkCase frameworks={["html"]}>
```css
media-time-slider::before {
width: calc(var(--media-slider-fill) * 1%);
width: var(--media-slider-fill);
}
```
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
React renders a `<div>` with the same data attributes and CSS custom properties. Add a `className` and use it as the selector:
React renders a `<div>` element. Add a `className` to style it:
```css
.time-slider::before {
width: calc(var(--media-slider-fill) * 1%);
width: var(--media-slider-fill);
}
```
</FrameworkCase>
+219
View File
@@ -0,0 +1,219 @@
---
title: Tooltip
frameworkTitle:
html: media-tooltip
description: A tooltip component for displaying contextual labels on hover and focus
---
import ComponentReference from "@/components/docs/api-reference/ComponentReference.astro";
import FrameworkCase from "@/components/docs/FrameworkCase.astro";
import StyleCase from "@/components/docs/StyleCase.astro";
import Demo from "@/components/docs/demos/Demo.astro";
{/* React demos */}
import BasicUsageDemoReact from "@/components/docs/demos/tooltip/react/css/BasicUsage";
import basicUsageReactTsx from "@/components/docs/demos/tooltip/react/css/BasicUsage.tsx?raw";
import basicUsageReactCss from "@/components/docs/demos/tooltip/react/css/BasicUsage.css?raw";
import GroupingDemoReact from "@/components/docs/demos/tooltip/react/css/Grouping";
import groupingReactTsx from "@/components/docs/demos/tooltip/react/css/Grouping.tsx?raw";
import groupingReactCss from "@/components/docs/demos/tooltip/react/css/Grouping.css?raw";
{/* HTML demos */}
import BasicUsageDemoHtml from "@/components/docs/demos/tooltip/html/css/BasicUsage.astro";
import basicUsageHtml from "@/components/docs/demos/tooltip/html/css/BasicUsage.html?raw";
import basicUsageHtmlCss from "@/components/docs/demos/tooltip/html/css/BasicUsage.css?raw";
import basicUsageHtmlTs from "@/components/docs/demos/tooltip/html/css/BasicUsage.ts?raw";
import GroupingDemoHtml from "@/components/docs/demos/tooltip/html/css/Grouping.astro";
import groupingHtml from "@/components/docs/demos/tooltip/html/css/Grouping.html?raw";
import groupingHtmlCss from "@/components/docs/demos/tooltip/html/css/Grouping.css?raw";
import groupingHtmlTs from "@/components/docs/demos/tooltip/html/css/Grouping.ts?raw";
## Anatomy
<FrameworkCase frameworks={["react"]}>
```tsx
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger>Hover me</Tooltip.Trigger>
<Tooltip.Popup>
<Tooltip.Arrow />
Label text
</Tooltip.Popup>
</Tooltip.Root>
</Tooltip.Provider>
```
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
```html
<media-tooltip-group>
<button commandfor="my-tooltip">Hover me</button>
<media-tooltip id="my-tooltip">Label text</media-tooltip>
</media-tooltip-group>
```
</FrameworkCase>
## Behavior
Displays a short label anchored to a trigger element. Opens after a configurable `delay` (default 600ms) on hover or immediately on focus. Closes when the pointer leaves or focus moves away, with an optional `closeDelay`.
The `side` and `align` props control placement relative to the trigger. Positioning uses CSS Anchor Positioning where supported, with a JavaScript measurement fallback.
<FrameworkCase frameworks={["react"]}>
The component is composed from four parts: `Root` manages state and context,
`Trigger` renders a button that activates the tooltip, `Popup` contains the label content,
and `Arrow` renders a decorative pointer. Wrap multiple tooltips in a `Tooltip.Provider`
to coordinate open/close timing across a group — once a tooltip becomes visible, adjacent
tooltips open instantly within the `timeout` window, skipping the normal `delay`.
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
The `<media-tooltip>` element is the popup itself. Link it to a trigger using
the `commandfor` attribute on any button, pointing to the tooltip's `id`. The element
discovers its trigger automatically and manages open/close state, ARIA attributes, and
positioning. Wrap tooltip trigger/popup pairs in `<media-tooltip-group>` to coordinate
timing — the group's `delay`, `close-delay`, and `timeout` attributes control shared
timing for all contained tooltips.
</FrameworkCase>
## Styling
Use [CSS custom properties](#root-css-custom-properties) for positioning offsets:
<FrameworkCase frameworks={["html"]}>
```css
media-tooltip {
--media-tooltip-side-offset: 8px;
--media-tooltip-align-offset: 0px;
}
```
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
React renders standard DOM elements. Add a `className` to style them:
```css
.tooltip-popup {
--media-tooltip-side-offset: 8px;
--media-tooltip-align-offset: 0px;
}
```
</FrameworkCase>
Style based on open state and transition phases:
<FrameworkCase frameworks={["html"]}>
```css
media-tooltip[data-open] {
display: block;
}
media-tooltip[data-starting-style] {
opacity: 0;
}
media-tooltip[data-ending-style] {
opacity: 0;
}
media-tooltip[data-side="top"] {
transform-origin: bottom center;
}
media-tooltip[data-side="bottom"] {
transform-origin: top center;
}
```
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
```css
.tooltip-popup[data-open] {
display: block;
}
.tooltip-popup[data-starting-style] {
opacity: 0;
}
.tooltip-popup[data-ending-style] {
opacity: 0;
}
.tooltip-popup[data-side="top"] {
transform-origin: bottom center;
}
.tooltip-popup[data-side="bottom"] {
transform-origin: top center;
}
```
</FrameworkCase>
## Accessibility
The trigger receives `aria-describedby` pointing to the popup when open. The popup renders with `role="tooltip"` and `popover="manual"`. Tooltips open on focus and close when focus leaves, ensuring keyboard-only users can access the label.
## Examples
### Basic Usage
<FrameworkCase frameworks={["react"]}>
<StyleCase styles={["css"]}>
<Demo
files={[
{ title: "App.tsx", code: basicUsageReactTsx, lang: "tsx" },
{ title: "App.css", code: basicUsageReactCss, lang: "css" },
]}
>
<BasicUsageDemoReact client:idle />
</Demo>
</StyleCase>
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
<StyleCase styles={["css"]}>
<Demo
files={[
{ title: "index.html", code: basicUsageHtml, lang: "html" },
{ title: "index.css", code: basicUsageHtmlCss, lang: "css" },
{ title: "index.ts", code: basicUsageHtmlTs, lang: "ts" },
]}
>
<BasicUsageDemoHtml />
</Demo>
</StyleCase>
</FrameworkCase>
### Grouping
<FrameworkCase frameworks={["react"]}>
Wrap multiple tooltips in a `Tooltip.Provider` to share a delay group. Once a tooltip
becomes visible, adjacent tooltips open instantly within the `timeout` window, skipping
the normal `delay`.
<StyleCase styles={["css"]}>
<Demo
files={[
{ title: "App.tsx", code: groupingReactTsx, lang: "tsx" },
{ title: "App.css", code: groupingReactCss, lang: "css" },
]}
>
<GroupingDemoReact client:idle />
</Demo>
</StyleCase>
</FrameworkCase>
<FrameworkCase frameworks={["html"]}>
Wrap tooltip trigger/popup pairs in `<media-tooltip-group>` to coordinate timing. The
group's `delay`, `close-delay`, and `timeout` attributes control shared timing for all
contained tooltips.
<StyleCase styles={["css"]}>
<Demo
files={[
{ title: "index.html", code: groupingHtml, lang: "html" },
{ title: "index.css", code: groupingHtmlCss, lang: "css" },
{ title: "index.ts", code: groupingHtmlTs, lang: "ts" },
]}
>
<GroupingDemoHtml />
</Demo>
</StyleCase>
</FrameworkCase>
<ComponentReference component="Tooltip" partOrder={["provider", "root", "trigger", "popup", "arrow"]} />
@@ -52,7 +52,7 @@ media-volume-slider::before {
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
React renders a `<div>` with the same data attributes and CSS custom properties. Add a `className` and use it as the selector:
React renders a `<div>` element. Add a `className` to style it:
```css
.volume-slider::before {
+2
View File
@@ -55,9 +55,11 @@ export const sidebar: Sidebar = [
{ slug: 'reference/popover' },
{ slug: 'reference/poster' },
{ slug: 'reference/seek-button' },
{ slug: 'reference/slider' },
{ slug: 'reference/thumbnail' },
{ slug: 'reference/time' },
{ slug: 'reference/time-slider' },
{ slug: 'reference/tooltip' },
{ slug: 'reference/volume-slider' },
],
},
+13 -2
View File
@@ -82,7 +82,7 @@ function createSections(source, options) {
* The shared model is what prevents anchor drift: ids are computed once and
* reused verbatim by the renderer and the remark plugin.
*/
export function createComponentReferenceModel(componentName, apiReference) {
export function createComponentReferenceModel(componentName, apiReference, partOrder) {
if (!apiReference) {
return null;
}
@@ -90,7 +90,18 @@ export function createComponentReferenceModel(componentName, apiReference) {
const hasParts = Boolean(apiReference.parts && Object.keys(apiReference.parts).length > 0);
if (hasParts) {
const parts = Object.entries(apiReference.parts).map(([partId, part]) => ({
let partEntries = Object.entries(apiReference.parts);
if (partOrder) {
const orderMap = new Map(partOrder.map((id, i) => [id, i]));
partEntries = partEntries.slice().sort((a, b) => {
const ai = orderMap.has(a[0]) ? orderMap.get(a[0]) : Number.MAX_SAFE_INTEGER;
const bi = orderMap.has(b[0]) ? orderMap.get(b[0]) : Number.MAX_SAFE_INTEGER;
return ai - bi;
});
}
const parts = partEntries.map(([partId, part]) => ({
id: partId,
name: part.name,
description: part.description,
+4 -1
View File
@@ -136,7 +136,10 @@ function injectComponentReferenceHeadings(node, headingsWithMetadata, reservedSl
const json = readComponentRefJson(componentName);
if (!json) return;
const componentModel = createComponentReferenceModel(componentName, json);
const partOrderAttr = node.attributes?.find((a) => a.name === 'partOrder');
const partOrder = extractArrayValue(partOrderAttr);
const componentModel = createComponentReferenceModel(componentName, json, partOrder);
const componentHeadings = buildComponentReferenceTocHeadings(componentModel);
headingsWithMetadata.push(...componentHeadings);