diff --git a/packages/html/html-icons/src/index.ts b/packages/html/html-icons/src/index.ts
index a0eb7777..1abb24cc 100644
--- a/packages/html/html-icons/src/index.ts
+++ b/packages/html/html-icons/src/index.ts
@@ -2,4 +2,6 @@ export * as MediaPlayIcon from './media-play-icon.js';
export * as MediaPauseIcon from './media-pause-icon.js';
export * as MediaVolumeHighIcon from './media-volume-high-icon.js';
export * as MediaVolumeLowIcon from './media-volume-low-icon.js';
-export * as MediaVolumeOffIcon from './media-volume-off-icon.js';
\ No newline at end of file
+export * as MediaVolumeOffIcon from './media-volume-off-icon.js';
+export * as MediaFullscreenEnterIcon from './media-fullscreen-enter-icon.js';
+export * as MediaFullscreenExitIcon from './media-fullscreen-exit-icon.js';
\ No newline at end of file
diff --git a/packages/html/html-icons/src/media-fullscreen-enter-icon.ts b/packages/html/html-icons/src/media-fullscreen-enter-icon.ts
new file mode 100644
index 00000000..b9ac632a
--- /dev/null
+++ b/packages/html/html-icons/src/media-fullscreen-enter-icon.ts
@@ -0,0 +1,20 @@
+import { MediaChromeIcon } from './media-chrome-icon.js';
+import { SVG_ICONS } from '@vjs-10/icons';
+
+export function getTemplateHTML() {
+ return /* html */ `
+ ${MediaChromeIcon.getTemplateHTML()}
+
+ ${SVG_ICONS.fullscreenEnter}
+ `;
+}
+
+export class MediaFullscreenEnterIcon extends MediaChromeIcon {
+ static getTemplateHTML = getTemplateHTML;
+}
+
+customElements.define('media-fullscreen-enter-icon', MediaFullscreenEnterIcon);
\ No newline at end of file
diff --git a/packages/html/html-icons/src/media-fullscreen-exit-icon.ts b/packages/html/html-icons/src/media-fullscreen-exit-icon.ts
new file mode 100644
index 00000000..1042298b
--- /dev/null
+++ b/packages/html/html-icons/src/media-fullscreen-exit-icon.ts
@@ -0,0 +1,20 @@
+import { MediaChromeIcon } from './media-chrome-icon.js';
+import { SVG_ICONS } from '@vjs-10/icons';
+
+export function getTemplateHTML() {
+ return /* html */ `
+ ${MediaChromeIcon.getTemplateHTML()}
+
+ ${SVG_ICONS.fullscreenExit}
+ `;
+}
+
+export class MediaFullscreenExitIcon extends MediaChromeIcon {
+ static getTemplateHTML = getTemplateHTML;
+}
+
+customElements.define('media-fullscreen-exit-icon', MediaFullscreenExitIcon);
\ No newline at end of file
diff --git a/packages/html/html/src/components/media-fullscreen-button.ts b/packages/html/html/src/components/media-fullscreen-button.ts
new file mode 100644
index 00000000..bfcea94c
--- /dev/null
+++ b/packages/html/html/src/components/media-fullscreen-button.ts
@@ -0,0 +1,101 @@
+import {
+ toConnectedHTMLComponent,
+ StateHook,
+ PropsHook,
+} from '../utils/component-factory';
+import { MediaChromeButton } from './media-chrome-button';
+import { fullscreenButtonStateDefinition } from '@vjs-10/media-store';
+
+export class FullscreenButtonBase extends MediaChromeButton {
+ _state:
+ | {
+ fullscreen: boolean;
+ requestEnterFullscreen: () => void;
+ requestExitFullscreen: () => void;
+ }
+ | undefined;
+
+ handleEvent(event: Event) {
+ const { type } = event;
+ const state = this._state;
+ if (state && type === 'click') {
+ if (state.fullscreen) {
+ state.requestExitFullscreen();
+ } else {
+ state.requestEnterFullscreen();
+ }
+ }
+ }
+
+ get fullscreen() {
+ return this._state?.fullscreen;
+ }
+
+ _update(props: any, state: any, _mediaStore?: any) {
+ this._state = state;
+ /** @TODO Follow up with React vs. W.C. data-* attributes discrepancies (CJP) */
+ // Make generic
+ this.toggleAttribute('data-fullscreen', props['data-fullscreen']);
+ this.setAttribute('role', props['role']);
+ this.setAttribute('aria-label', props['aria-label']);
+ this.setAttribute('data-tooltip', props['data-tooltip']);
+ }
+}
+
+/**
+ * FullscreenButton state hook - equivalent to React's useFullscreenButtonState
+ * Handles media store state subscription and transformation
+ */
+export const useFullscreenButtonState: StateHook<{ fullscreen: boolean }> = {
+ keys: fullscreenButtonStateDefinition.keys,
+ transform: (rawState, mediaStore) => ({
+ ...fullscreenButtonStateDefinition.stateTransform(rawState),
+ ...fullscreenButtonStateDefinition.createRequestMethods(
+ mediaStore.dispatch,
+ ),
+ }),
+};
+
+/**
+ * FullscreenButton props hook - equivalent to React's useFullscreenButtonProps
+ * Handles element attributes and properties based on state
+ */
+export const useFullscreenButtonProps: PropsHook<{ fullscreen: boolean }> = (
+ state,
+ _element,
+) => {
+ const baseProps: Record = {
+ /** data attributes/props */
+ ['data-fullscreen']: state.fullscreen,
+ /** @TODO Need another state provider in core for i18n (CJP) */
+ /** aria attributes/props */
+ role: 'button',
+ ['aria-label']: state.fullscreen ? 'exit fullscreen' : 'enter fullscreen',
+ /** tooltip */
+ ['data-tooltip']: state.fullscreen ? 'Exit Fullscreen' : 'Enter Fullscreen',
+ /** @TODO Figure out how we want to handle attr overrides (e.g. aria-label) (CJP) */
+ /** external props spread last to allow for overriding */
+ // ...props,
+ };
+
+ return baseProps;
+};
+
+/**
+ * Connected FullscreenButton component using hook-style architecture
+ * Equivalent to React's FullscreenButton = toConnectedComponent(...)
+ */
+export const FullscreenButton = toConnectedHTMLComponent(
+ FullscreenButtonBase,
+ useFullscreenButtonState,
+ useFullscreenButtonProps,
+ 'FullscreenButton',
+);
+
+// NOTE: In this architecture it will be important to decouple component class definitions from their registration in the CustomElementsRegistry. (CJP)
+if (!globalThis.customElements.get('media-fullscreen-button')) {
+ // @ts-ignore - Custom element constructor compatibility
+ globalThis.customElements.define('media-fullscreen-button', FullscreenButton);
+}
+
+export default FullscreenButton;
diff --git a/packages/html/html/src/index.ts b/packages/html/html/src/index.ts
index 2185fd2f..34ea94ba 100644
--- a/packages/html/html/src/index.ts
+++ b/packages/html/html/src/index.ts
@@ -5,6 +5,7 @@ export * as MediaThemeDefault from './skins/media-skin-default.js';
export { PlayButton } from './components/media-play-button.js';
export { MuteButton } from './components/media-mute-button.js';
export { VolumeRange } from './components/media-volume-range.js';
+export { FullscreenButton } from './components/media-fullscreen-button.js';
export function defineVjsPlayer() {
/** @TODO - Reimplement me (at least as a POC) (CJP) */