This article presents how to create a clean, light, and professional video player Web Part for SharePoint Online using the SharePoint Framework (SPFx) and Fluent UI React components.
The result is a visually elegant player that dynamically loads .mp4 files from any selected document library, allowing users to search, preview, and play videos directly inside a modern SharePoint page.
🎬 Building Fluent Video Hub: a Modern SharePoint Video Player with Fluent UI
Overview
This article presents how to create a clean, light, and professional video player Web Part for SharePoint Online using the SharePoint Framework (SPFx) and Fluent UI React components.
The result is a visually elegant player that dynamically loads .mp4 files from any selected document library, allowing users to search, preview, and play videos directly inside a modern SharePoint page.
🧩 1️⃣ Concept and Motivation
SharePoint libraries often host valuable training and communication videos, but browsing them through the native UI is cumbersome.Fluent Video Hub solves that by providing a modern and brand-neutral interface built with Fluent UI:
- 🟨 Yellow accent inspired by Microsoft’s Fluent Design System
- 🧱 Minimalistic layout (white, gray, light shadows)
- 🎞️ Dynamic loading of
.mp4files - 🔍 Search bar and thumbnail preview
- 🖥️ Inline playback in a Fluent
Panel(fullscreen-like experience)
Unlike traditional SPFx web parts with heavy SCSS and dark backgrounds, this one uses pure Fluent UI styling for maximum performance and accessibility.
⚙️ 2️⃣ Project Setup
In your SharePoint Framework development environment:
yo @microsoft/sharepoint
Choose:
? Solution Name: FluentVideoHub
? Web Part Name: FluentVideoHub
? Framework: React
Then install Fluent UI:
npm install @fluentui/react @fluentui/react-components
🧠 3️⃣ Architecture Overview
| Layer | Description |
|---|---|
| WebPart class | Connects the property pane to React component |
| React component | Handles UI, state, and logic for loading and displaying videos |
| Fluent UI components | Provide the entire visual structure (Stack, TextField, Panel, Image, etc.) |
| SharePoint REST API | Fetches .mp4 files from a document library |
HTML5 <video> | Plays selected file inline |
🧱 4️⃣ The WebPart (FluentVideoHubWebPart.ts)
This file handles configuration through the Property Pane, allowing the user to select any document library dynamically.
import { Version } from '@microsoft/sp-core-library';
import { IPropertyPaneConfiguration, PropertyPaneDropdown } from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { SPHttpClient } from '@microsoft/sp-http';
import FluentVideoHub from './components/FluentVideoHub';
import { IFluentVideoHubProps } from './components/IFluentVideoHubProps';
export interface IFluentVideoHubWebPartProps {
libraryName: string;
}
export default class FluentVideoHubWebPart extends BaseClientSideWebPart<IFluentVideoHubWebPartProps> {
private libraryOptions: { key: string; text: string; }[] = [];
public render(): void {
const element: React.ReactElement<IFluentVideoHubProps> = React.createElement(
FluentVideoHub,
{ context: this.context, libraryName: this.properties.libraryName }
);
ReactDom.render(element, this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
private async loadLibraries(): Promise<void> {
if (this.libraryOptions.length > 0) return;
const url = `${this.context.pageContext.web.absoluteUrl}/_api/web/lists?$filter=BaseTemplate eq 101 and Hidden eq false&$select=Title`;
const res = await this.context.spHttpClient.get(url, SPHttpClient.configurations.v1);
const data = await res.json();
this.libraryOptions = data.value.map((l: any) => ({ key: l.Title, text: l.Title }));
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
if (this.libraryOptions.length === 0) {
this.loadLibraries().then(() => this.context.propertyPane.refresh());
}
return {
pages: [{
header: { description: "Fluent Video Hub Settings" },
groups: [{
groupName: "Library Source",
groupFields: [
PropertyPaneDropdown('libraryName', {
label: 'Select Library',
options: this.libraryOptions.length ? this.libraryOptions : [{ key: '', text: 'Loading...' }]
})
]
}]
}]
};
}
}
🎨 5️⃣ The Fluent UI Component (FluentVideoHub.tsx)
This component builds the gallery, search, and player — using only Fluent UI React.
import * as React from 'react';
import { useState, useEffect } from 'react';
import { SPHttpClient } from '@microsoft/sp-http';
import { IFluentVideoHubProps } from './IFluentVideoHubProps';
import {
Stack, TextField, IconButton, Panel, PanelType, Text, Image
} from '@fluentui/react';
interface VideoItem {
title: string;
url: string;
}
const FluentVideoHub: React.FC<IFluentVideoHubProps> = ({ context, libraryName }) => {
const [videos, setVideos] = useState<VideoItem[]>([]);
const [filtered, setFiltered] = useState<VideoItem[]>([]);
const [current, setCurrent] = useState<VideoItem | null>(null);
const [query, setQuery] = useState('');
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const accent = '#FFD700';
// Load video files dynamically
useEffect(() => {
if (!libraryName) return;
setLoading(true);
const endpoint = `${context.pageContext.web.absoluteUrl}/_api/web/lists/getbytitle('${libraryName}')/items?$select=File/Name,File/ServerRelativeUrl&$expand=File`;
context.spHttpClient.get(endpoint, SPHttpClient.configurations.v1)
.then(res => res.json())
.then(data => {
const sitePath = context.pageContext.site.serverRelativeUrl.toLowerCase();
const vids = data.value
.filter((f: any) => f.File && f.File.Name.toLowerCase().endsWith('.mp4'))
.map((f: any) => {
let rel = f.File.ServerRelativeUrl as string;
const pattern = new RegExp(`${sitePath}${sitePath}`, 'i');
if (pattern.test(rel.toLowerCase())) rel = rel.replace(pattern, sitePath);
return {
title: f.File.Name,
url: `${context.pageContext.site.absoluteUrl}${encodeURI(rel)}`
};
});
setVideos(vids);
setFiltered(vids);
})
.catch(console.error)
.then(() => setLoading(false));
}, [libraryName]);
useEffect(() => {
const q = query.toLowerCase();
setFiltered(videos.filter(v => v.title.toLowerCase().includes(q)));
}, [query, videos]);
return (
<Stack tokens={{ childrenGap: 16 }} styles={{ root: { padding: 20, background: '#fff', color: '#000' } }}>
<Stack horizontal horizontalAlign="space-between" verticalAlign="end">
<Text variant="xLarge" styles={{ root: { fontWeight: 600 } }}>Fluent Video Hub</Text>
<TextField
placeholder="Search videos..."
value={query}
onChange={(_, v) => setQuery(v || '')}
styles={{ root: { width: 260 } }}
/>
</Stack>
{loading && <Text>Loading videos...</Text>}
{!loading && (
<Stack horizontal wrap tokens={{ childrenGap: 12 }}>
{filtered.map((v, i) => (
<Stack
key={i}
styles={{
root: {
width: 200,
background: '#f8f8f8',
borderRadius: 8,
boxShadow: '0 2px 6px rgba(0,0,0,0.1)',
cursor: 'pointer',
padding: 8,
transition: 'transform 0.2s',
border: `2px solid ${accent}`
}
}}
onClick={() => { setCurrent(v); setOpen(true); }}
>
<Image
src={`${context.pageContext.site.absoluteUrl}/_layouts/15/getpreview.ashx?path=${encodeURIComponent(v.url)}`}
alt={v.title}
height={120}
imageFit={3}
styles={{ root: { borderRadius: 6, marginBottom: 6 } }}
/>
<Text styles={{ root: { fontWeight: 600, fontSize: 14 } }}>{v.title}</Text>
</Stack>
))}
</Stack>
)}
<Panel
isOpen={open}
type={PanelType.large}
onDismiss={() => setOpen(false)}
headerText={current?.title}
isLightDismiss
styles={{ main: { background: '#fff' } }}
>
{current && (
<Stack tokens={{ childrenGap: 8 }}>
<video
src={current.url}
controls
style={{ width: '100%', borderRadius: 8, border: `3px solid ${accent}` }}
/>
<Stack horizontal horizontalAlign="space-between" verticalAlign="center">
<Text variant="medium">{current.title}</Text>
<IconButton iconProps={{ iconName: 'Like' }} title="Like" ariaLabel="Like" />
</Stack>
</Stack>
)}
</Panel>
</Stack>
);
};
export default FluentVideoHub;
🧠 6️⃣ Design Highlights
| Element | Fluent UI Component | Purpose |
|---|---|---|
| Header + Search | Stack + TextField | clean top bar |
| Video gallery | Stack horizontal wrap | card-based grid |
| Thumbnail | Image with SharePoint preview endpoint | lightweight preview |
| Player | HTML <video> inside Panel | fullscreen-like player |
| Accent color | #FFD700 (yellow) | visual identity |
📊 7️⃣ Feature Summary
| Feature | Status | Description |
|---|---|---|
| Dynamic Library Loading | ✅ | Select any document library from property pane |
| Search Filtering | ✅ | Case-insensitive |
| Preview Thumbnails | ✅ | Uses _layouts/15/getpreview.ashx |
| Inline Player | ✅ | Opens in Fluent UI Panel |
| Fluent UI Design | ✅ | Light, responsive, minimal code |
| Performance | ⚡ | No extra CSS files, no external libraries |
⚙️ 8️⃣ Deployment
gulp clean
gulp build
gulp bundle --ship
gulp package-solution --ship
Upload your .sppkg → App Catalog → add the Web Part → configure the library.
💡 9️⃣ Future Enhancements
- Add carousel scrolling arrows using
ScrollButtonsfrom Fluent UI - Implement like persistence with
localStorage - Integrate analytics (Graph API view count)
- Add responsive breakpoints with
useMediaQuery - Create a dark mode version via Fluent theme tokens
🏁 10️⃣ Conclusion
Fluent Video Hub demonstrates how to blend SPFx, Fluent UI, and REST APIs into a clean, efficient Web Part that feels modern, accessible, and lightweight.
It’s ideal for intranet training hubs, corporate communications, or any environment where users browse and play SharePoint-hosted videos.
The Fluent UI approach allows developers to avoid complex SCSS styling while ensuring visual consistency and accessibility across Microsoft 365.
