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 .mp4 files
  • 🔍 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

LayerDescription
WebPart classConnects the property pane to React component
React componentHandles UI, state, and logic for loading and displaying videos
Fluent UI componentsProvide the entire visual structure (Stack, TextField, Panel, Image, etc.)
SharePoint REST APIFetches .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

ElementFluent UI ComponentPurpose
Header + SearchStack + TextFieldclean top bar
Video galleryStack horizontal wrapcard-based grid
ThumbnailImage with SharePoint preview endpointlightweight preview
PlayerHTML <video> inside Panelfullscreen-like player
Accent color#FFD700 (yellow)visual identity

📊 7️⃣ Feature Summary

FeatureStatusDescription
Dynamic Library LoadingSelect any document library from property pane
Search FilteringCase-insensitive
Preview ThumbnailsUses _layouts/15/getpreview.ashx
Inline PlayerOpens in Fluent UI Panel
Fluent UI DesignLight, responsive, minimal code
PerformanceNo 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 ScrollButtons from 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.

Edvaldo Guimrães Filho Avatar

Published by