You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
`BannerVideo` renders a full-width banner section that plays a muted, looping mp4 video. A poster image is shown as fallback when:
11
+
`BannerVideo` renders a full-width banner section that plays a muted, looping mp4 video. A poster image is shown as fallback when the user has `prefers-reduced-motion: reduce` set — handled entirely in CSS, no JS.
12
12
13
-
- The video fails to load or play
14
-
- The user has `prefers-reduced-motion: reduce` set (handled entirely in CSS — no JS)
13
+
The banner is sized via `aspect-ratio` so it scales naturally, with `max-height` props capping height at each breakpoint.
15
14
16
-
The banner is sized via `aspect-ratio` so it scales naturally, with `max-height` props capping height at each breakpoint. Both `objectFit` and `objectPosition` apply directly to the `<video>` element, which is a CSS replaced element and behaves exactly like `<img>`.
15
+
### Autoplay mechanism
16
+
17
+
The video uses `autoplay muted loop playsinline preload="auto"` attributes on the `<video>` element with the src in a `<source>` child. A `:key="src"` on the video element forces Vue to remount it on src changes. `@loadeddata` and `@canplay` events call `tryPlay()`, and a `watch(() => props.src, ..., { immediate: true, flush: 'post' })` calls `kickOffLoad()` (which calls `v.load()` then `tryPlay()`) on mount and src change. `onActivated` handles keep-alive re-activation.
17
18
18
19
## Props
19
20
20
21
| Prop | Type | Default | Description |
21
22
|------|------|---------|-------------|
22
23
|`src`|`string`| — | Path to the mp4 video source. **Required.**|
23
-
|`poster`|`string`| — | Path to the fallback/poster image. **Required.** Used as the video poster attribute and as the visible fallback. |
24
+
|`poster`|`string`| — | Path to the fallback/poster image. **Required.** Used as the video `poster` attribute and as the `prefers-reduced-motion` fallback. |
24
25
|`alt`|`string`|`""`| Alt text for the fallback `NuxtImg`. |
25
26
|`imgWidth`|`number`|`1920`| Intrinsic width of the poster image — required for NuxtImg/IPX optimisation. |
26
27
|`imgHeight`|`number`|`1080`| Intrinsic height of the poster image — required for NuxtImg/IPX optimisation. |
27
28
|`tag`|`"section" \| "div" \| "header" \| "main" \| "article"`|`"section"`| HTML element rendered as the root. |
28
29
|`maxHeight`|`string`|`"56rem"`| Maximum height at desktop (≥64em / 1024px). |
29
-
|`maxHeightTablet`|`string`|`undefined`| Maximum height at tablet (48em–64em / 768px–1024px). Falls back to `maxHeight`. |
30
-
|`maxHeightMobile`|`string`|`undefined`| Maximum height on mobile (<48em / 768px). Falls back through tablet → desktop. |
30
+
|`maxHeightTablet`|`string`|`undefined`| Maximum height at tablet (48em–64em). Falls back to `maxHeight`. |
31
+
|`maxHeightMobile`|`string`|`undefined`| Maximum height on mobile (<48em). Falls back through tablet → desktop. |
31
32
|`aspectRatio`|`string`|`"21/9"`| CSS `aspect-ratio` of the container (e.g. `"16/9"`, `"21/9"`, `"4/3"`). |
32
33
|`objectFit`|`"cover" \| "contain" \| "fill" \| "none" \| "scale-down"`|`"cover"`| How the video and fallback image fill the banner frame. |
33
-
|`objectPosition`|`string`|`"50% 50%"`| Focal point within the video and fallback image — any valid CSS `object-position` value. |
34
+
|`verticalPosition`|`"start" \| "center" \| "end"`|`"center"`| Vertical crop position. Maps to `align-self` on the video element and `object-position` Y on the fallback image. |
35
+
|`horizontalPosition`|`"start" \| "center" \| "end"`|`"center"`| Horizontal crop position. Maps to `object-position` X on the fallback image. |
34
36
|`styleClassPassthrough`|`string \| string[]`|`[]`| Extra classes applied to the root element. |
35
37
36
38
## Basic usage
@@ -70,25 +72,15 @@ The banner is sized via `aspect-ratio` so it scales naturally, with `max-height`
70
72
/>
71
73
```
72
74
73
-
### Standard 16/9 crop
74
-
75
-
```vue
76
-
<BannerVideo
77
-
src="/videos/hero.mp4"
78
-
poster="/images/hero-poster.jpg"
79
-
alt="Studio interior"
80
-
aspect-ratio="16/9"
81
-
/>
82
-
```
83
-
84
75
### Custom focal point
85
76
86
77
```vue
87
78
<BannerVideo
88
79
src="/videos/hero.mp4"
89
80
poster="/images/hero-poster.jpg"
90
81
alt="Studio interior"
91
-
object-position="50% 75%"
82
+
vertical-position="end"
83
+
horizontal-position="center"
92
84
/>
93
85
```
94
86
@@ -103,19 +95,23 @@ The banner is sized via `aspect-ratio` so it scales naturally, with `max-height`
103
95
/>
104
96
```
105
97
106
-
## objectPosition explained
98
+
## Positioning explained
107
99
108
-
`objectPosition` only produces a visible result when the video is **cropped** — i.e. when the container's aspect ratio differs from the video's native ratio, causing `object-fit: cover` to overflow.
100
+
The video element uses `height: auto; min-height: 100%` so it overflows the container naturally when aspect ratios differ. `overflow: hidden` on the root clips it. `verticalPosition` maps to `align-self` on the video (shifting which portion of the overflow is visible) and to `object-position` Y on the fallback image.
109
101
110
-
- Default `aspectRatio: "21/9"` with a 16:9 video → always crops vertically → `objectPosition` Y value controls which vertical slice is shown.
111
-
-`aspectRatio: "16/9"` with a 16:9 video → no overflow → `objectPosition` has no visible effect.
102
+
`horizontalPosition` only affects the fallback image via `object-position` X — the video fills full width so horizontal alignment is a no-op on the video element itself.
112
103
113
-
| objectPosition | Effect with 21/9 container + 16:9 video |
104
+
| verticalPosition | align-self | object-position Y |
105
+
|---|---|---|
106
+
|`"start"`|`start`|`top`|
107
+
|`"center"` (default) |`center`|`center`|
108
+
|`"end"`|`end`|`bottom`|
109
+
110
+
| horizontalPosition | object-position X |
114
111
|---|---|
115
-
|`"50% 0%"`| Top of the frame locked in view |
116
-
|`"50% 50%"` (default) | Centre of the frame |
117
-
|`"50% 100%"`| Bottom of the frame locked in view |
118
-
|`"0% 50%"`| Left-biased horizontal crop |
112
+
|`"start"`|`left`|
113
+
|`"center"` (default) |`center`|
114
+
|`"end"`|`right`|
119
115
120
116
## imgWidth / imgHeight
121
117
@@ -129,14 +125,16 @@ Always match the intrinsic dimensions of the poster file. NuxtImg uses them to a
129
125
130
126
## CSS custom properties
131
127
132
-
All set from props via inline `:style` on the root element. Override in a scoped style block for responsive or contextual control.
128
+
All set from props via inline `:style` on the root element.
@@ -162,10 +160,7 @@ All set from props via inline `:style` on the root element. Override in a scoped
162
160
|---|---|
163
161
| `.banner-video` | Always — the root element |
164
162
| `.video` | The `<video>` element |
165
-
| `.fallback` | The `<NuxtImg>` fallback element |
166
-
| `.video-failed` | Added to root when the video or source fires an error event |
167
-
168
-
When `.video-failed` is present, `.video` is hidden and `.fallback` is shown via CSS. The same swap happens at `@media (prefers-reduced-motion: reduce)` — no JS involved.
163
+
| `.fallback` | The `<NuxtImg>` fallback — hidden by default, shown via `prefers-reduced-motion` CSS |
169
164
170
165
## Consumer styling
171
166
@@ -175,7 +170,6 @@ Use an unscoped style block scoped by a page or section wrapper class. No `:deep
175
170
<style>
176
171
.my-page {
177
172
.banner-video {
178
-
/* Override max-height at a custom breakpoint */
179
173
@media (width < 900px) {
180
174
--_max-height: 36rem;
181
175
}
@@ -187,7 +181,6 @@ Use an unscoped style block scoped by a page or section wrapper class. No `:deep
187
181
## Notes
188
182
189
183
- `loading="eager"` and `decoding="async"` are hardcoded on the fallback `NuxtImg` — it is above the fold by definition.
190
-
- The video has `autoplay muted loop playsinline` attributes. These are intentional and not configurable — this component is for ambient background video only, not user-controlled media.
184
+
- The video has `autoplay muted loop playsinline preload="auto"` — intentional and not configurable. This component is for ambient background video only, not user-controlled media.
191
185
- `prefers-reduced-motion` is handled in CSS (`.video { display: none }` + `.fallback { display: block }`), not via JS.
192
-
- Error detection covers both `@error` on `<video>` and `@error` on `<source>` for cross-browser reliability.
193
186
- Storybook: the `"none"` image provider is active, so `poster` paths pass through unchanged. Always provide explicit `img-width` and `img-height` to avoid the `w=1536` fallback in deployed Storybook.
0 commit comments