diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/detached-trigger/css-modules/index.module.css b/docs/src/app/(docs)/react/components/fullscreen/demos/detached-trigger/css-modules/index.module.css new file mode 100644 index 00000000000..9f6bd4e5e7d --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/detached-trigger/css-modules/index.module.css @@ -0,0 +1,110 @@ +.Layout { + display: flex; + flex-direction: column; + gap: 0.5rem; + width: 18rem; +} + +.Header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 0.75rem; + padding: 0 0.125rem; +} + +.Label { + display: inline-flex; + align-items: center; + gap: 0.5rem; + margin: 0; + font-size: 0.8125rem; + line-height: 1.25rem; + font-weight: 500; + color: var(--color-gray-700); +} + +.LabelDot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background-color: var(--color-blue); +} + +.Container { + position: relative; + display: flex; + width: 100%; + aspect-ratio: 16 / 10; + align-items: center; + justify-content: center; + border: 1px solid var(--color-gray-200); + border-radius: 0.5rem; + background-color: var(--color-gray-50); + background-image: radial-gradient(circle at 50% 30%, var(--color-gray-100) 0%, transparent 70%); + color: var(--color-gray-900); + overflow: hidden; +} + +.Container[data-fullscreen] { + width: 100vw; + height: 100vh; + aspect-ratio: auto; + border-radius: 0; + border-color: transparent; +} + +.Trigger, +.Close { + box-sizing: border-box; + display: inline-flex; + align-items: center; + gap: 0.375rem; + height: 2rem; + margin: 0; + padding: 0 0.625rem; + border: 1px solid var(--color-gray-200); + border-radius: 0.375rem; + background-color: var(--color-gray-50); + color: var(--color-gray-900); + font-family: inherit; + font-size: 0.8125rem; + line-height: 1.25rem; + font-weight: 500; + cursor: pointer; + user-select: none; + + @media (hover: hover) { + &:hover { + background-color: var(--color-gray-100); + } + } + + &:active { + background-color: var(--color-gray-100); + } + + &:focus-visible { + outline: 2px solid var(--color-blue); + outline-offset: -1px; + } + + &[data-disabled] { + cursor: not-allowed; + opacity: 0.5; + } +} + +.Trigger[data-fullscreen] { + display: none; +} + +.Close { + position: absolute; + top: 0.625rem; + right: 0.625rem; +} + +.Close[data-not-fullscreen] { + display: none; +} diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/detached-trigger/css-modules/index.tsx b/docs/src/app/(docs)/react/components/fullscreen/demos/detached-trigger/css-modules/index.tsx new file mode 100644 index 00000000000..0e621414162 --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/detached-trigger/css-modules/index.tsx @@ -0,0 +1,48 @@ +'use client'; +import * as React from 'react'; +import { Fullscreen } from '@base-ui/react/fullscreen'; +import styles from './index.module.css'; + +const playerFullscreen = Fullscreen.createHandle(); + +export default function ExampleFullscreenDetached() { + return ( +
+
+

+

+ + + Enter fullscreen + +
+ + + + + + Close + + + +
+ ); +} + +function ExpandIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} + +function CloseIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/detached-trigger/index.ts b/docs/src/app/(docs)/react/components/fullscreen/demos/detached-trigger/index.ts new file mode 100644 index 00000000000..9d5ce6cc708 --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/detached-trigger/index.ts @@ -0,0 +1,8 @@ +import { createDemoWithVariants } from 'docs/src/utils/createDemo'; +import CssModules from './css-modules'; +import Tailwind from './tailwind'; + +export const DemoFullscreenDetachedTrigger = createDemoWithVariants(import.meta.url, { + CssModules, + Tailwind, +}); diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/detached-trigger/tailwind/index.tsx b/docs/src/app/(docs)/react/components/fullscreen/demos/detached-trigger/tailwind/index.tsx new file mode 100644 index 00000000000..31375e3bbae --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/detached-trigger/tailwind/index.tsx @@ -0,0 +1,53 @@ +'use client'; +import * as React from 'react'; +import { Fullscreen } from '@base-ui/react/fullscreen'; + +const playerFullscreen = Fullscreen.createHandle(); + +export default function ExampleFullscreenDetached() { + return ( +
+
+

+

+ + + Enter fullscreen + +
+ + + + + Live broadcast + + + + Close + + + +
+ ); +} + +function ExpandIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} + +function CloseIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/hero/css-modules/index.module.css b/docs/src/app/(docs)/react/components/fullscreen/demos/hero/css-modules/index.module.css new file mode 100644 index 00000000000..45ed584beb3 --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/hero/css-modules/index.module.css @@ -0,0 +1,115 @@ +.Container { + position: relative; + display: flex; + width: 18rem; + aspect-ratio: 16 / 10; + align-items: center; + justify-content: center; + border: 1px solid var(--color-gray-200); + border-radius: 0.5rem; + background-color: var(--color-gray-50); + background-image: radial-gradient(circle at 50% 30%, var(--color-gray-100) 0%, transparent 70%); + color: var(--color-gray-900); + overflow: hidden; +} + +.Container[data-fullscreen] { + width: 100vw; + height: 100vh; + aspect-ratio: auto; + border-radius: 0; + border-color: transparent; +} + +.Caption { + position: absolute; + top: 0.625rem; + left: 0.75rem; + margin: 0; + font-size: 0.75rem; + line-height: 1rem; + letter-spacing: 0.02em; + text-transform: uppercase; + color: var(--color-gray-500); +} + +.Caption[data-fullscreen] { + display: none; +} + +.Content { + display: flex; + align-items: center; + gap: 0.75rem; + font-size: 0.9375rem; + line-height: 1.5rem; + font-weight: 500; +} + +.ContentDot { + width: 0.5rem; + height: 0.5rem; + border-radius: 50%; + background-color: var(--color-blue); +} + +.Trigger, +.Close { + box-sizing: border-box; + display: inline-flex; + align-items: center; + gap: 0.375rem; + height: 2rem; + margin: 0; + padding: 0 0.625rem; + border: 1px solid var(--color-gray-200); + border-radius: 0.375rem; + background-color: var(--color-gray-50); + color: var(--color-gray-900); + font-family: inherit; + font-size: 0.8125rem; + line-height: 1.25rem; + font-weight: 500; + cursor: pointer; + user-select: none; + + @media (hover: hover) { + &:hover { + background-color: var(--color-gray-100); + } + } + + &:active { + background-color: var(--color-gray-100); + } + + &:focus-visible { + outline: 2px solid var(--color-blue); + outline-offset: -1px; + } + + &[data-disabled] { + cursor: not-allowed; + opacity: 0.5; + } +} + +.Trigger { + position: absolute; + bottom: 0.625rem; + right: 0.625rem; +} + +.Trigger[data-fullscreen] { + display: none; +} + +.Close { + position: absolute; + top: 0.625rem; + right: 0.625rem; +} + +.Close[data-not-fullscreen] { + display: none; +} diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/hero/css-modules/index.tsx b/docs/src/app/(docs)/react/components/fullscreen/demos/hero/css-modules/index.tsx new file mode 100644 index 00000000000..c12c3b23cbd --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/hero/css-modules/index.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import { Fullscreen } from '@base-ui/react/fullscreen'; +import styles from './index.module.css'; + +export default function ExampleFullscreen() { + return ( + + +

Preview

+ + + + + + Enter fullscreen + + + + Close + +
+
+ ); +} + +function ExpandIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} + +function CloseIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/hero/index.ts b/docs/src/app/(docs)/react/components/fullscreen/demos/hero/index.ts new file mode 100644 index 00000000000..c80131f0ab6 --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/hero/index.ts @@ -0,0 +1,9 @@ +import { createDemoWithVariants } from 'docs/src/utils/createDemo'; +import CssModules from './css-modules'; +import Tailwind from './tailwind'; + +export const DemoFullscreenHero = createDemoWithVariants( + import.meta.url, + { CssModules, Tailwind }, + { highlightAfter: 'init', enhanceAfter: 'init' }, +); diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/hero/tailwind/index.tsx b/docs/src/app/(docs)/react/components/fullscreen/demos/hero/tailwind/index.tsx new file mode 100644 index 00000000000..fae983e1ff5 --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/hero/tailwind/index.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { Fullscreen } from '@base-ui/react/fullscreen'; + +export default function ExampleFullscreen() { + return ( + + +

+ Preview +

+ + + + + + Enter fullscreen + + + + Close + +
+
+ ); +} + +function ExpandIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} + +function CloseIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/page-fullscreen/css-modules/index.module.css b/docs/src/app/(docs)/react/components/fullscreen/demos/page-fullscreen/css-modules/index.module.css new file mode 100644 index 00000000000..e88001d6228 --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/page-fullscreen/css-modules/index.module.css @@ -0,0 +1,39 @@ +.Trigger { + box-sizing: border-box; + display: inline-flex; + align-items: center; + gap: 0.375rem; + height: 2rem; + margin: 0; + padding: 0 0.625rem; + border: 1px solid var(--color-gray-200); + border-radius: 0.375rem; + background-color: var(--color-gray-50); + color: var(--color-gray-900); + font-family: inherit; + font-size: 0.8125rem; + line-height: 1.25rem; + font-weight: 500; + cursor: pointer; + user-select: none; + + @media (hover: hover) { + &:hover { + background-color: var(--color-gray-100); + } + } + + &:active { + background-color: var(--color-gray-100); + } + + &:focus-visible { + outline: 2px solid var(--color-blue); + outline-offset: -1px; + } + + &[data-disabled] { + cursor: not-allowed; + opacity: 0.5; + } +} diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/page-fullscreen/css-modules/index.tsx b/docs/src/app/(docs)/react/components/fullscreen/demos/page-fullscreen/css-modules/index.tsx new file mode 100644 index 00000000000..2381ee91c8b --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/page-fullscreen/css-modules/index.tsx @@ -0,0 +1,30 @@ +'use client'; +import * as React from 'react'; +import { Fullscreen } from '@base-ui/react/fullscreen'; +import styles from './index.module.css'; + +export default function ExampleFullscreenPage() { + return ( + + + + Fullscreen the page + + + ); +} + +function getDocumentElement() { + if (typeof document === 'undefined') { + return null; + } + return document.documentElement; +} + +function ExpandIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/page-fullscreen/index.ts b/docs/src/app/(docs)/react/components/fullscreen/demos/page-fullscreen/index.ts new file mode 100644 index 00000000000..ba93759c2d8 --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/page-fullscreen/index.ts @@ -0,0 +1,8 @@ +import { createDemoWithVariants } from 'docs/src/utils/createDemo'; +import CssModules from './css-modules'; +import Tailwind from './tailwind'; + +export const DemoFullscreenPage = createDemoWithVariants(import.meta.url, { + CssModules, + Tailwind, +}); diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/page-fullscreen/tailwind/index.tsx b/docs/src/app/(docs)/react/components/fullscreen/demos/page-fullscreen/tailwind/index.tsx new file mode 100644 index 00000000000..25b03684325 --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/page-fullscreen/tailwind/index.tsx @@ -0,0 +1,29 @@ +'use client'; +import * as React from 'react'; +import { Fullscreen } from '@base-ui/react/fullscreen'; + +export default function ExampleFullscreenPage() { + return ( + + + + Fullscreen the page + + + ); +} + +function getDocumentElement() { + if (typeof document === 'undefined') { + return null; + } + return document.documentElement; +} + +function ExpandIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/portal/css-modules/index.module.css b/docs/src/app/(docs)/react/components/fullscreen/demos/portal/css-modules/index.module.css new file mode 100644 index 00000000000..896542753b6 --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/portal/css-modules/index.module.css @@ -0,0 +1,75 @@ +.Trigger, +.Close { + box-sizing: border-box; + display: inline-flex; + align-items: center; + gap: 0.375rem; + height: 2rem; + margin: 0; + padding: 0 0.625rem; + border: 1px solid var(--color-gray-200); + border-radius: 0.375rem; + background-color: var(--color-gray-50); + color: var(--color-gray-900); + font-family: inherit; + font-size: 0.8125rem; + line-height: 1.25rem; + font-weight: 500; + cursor: pointer; + user-select: none; + + @media (hover: hover) { + &:hover { + background-color: var(--color-gray-100); + } + } + + &:active { + background-color: var(--color-gray-100); + } + + &:focus-visible { + outline: 2px solid var(--color-blue); + outline-offset: -1px; + } + + &[data-disabled] { + cursor: not-allowed; + opacity: 0.5; + } +} + +.Container { + position: relative; + display: flex; + flex-direction: column; + width: 100vw; + height: 100vh; + align-items: center; + justify-content: center; + gap: 0.5rem; + background-color: var(--color-gray-50); + background-image: radial-gradient(circle at 50% 30%, var(--color-gray-100) 0%, transparent 70%); + color: var(--color-gray-900); +} + +.Title { + margin: 0; + font-size: 1.125rem; + line-height: 1.75rem; + font-weight: 600; + letter-spacing: -0.01em; +} + +.Description { + margin: 0; + font-size: 0.9375rem; + line-height: 1.5rem; + color: var(--color-gray-600); +} + +.Close { + position: absolute; + top: 1rem; + right: 1rem; +} diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/portal/css-modules/index.tsx b/docs/src/app/(docs)/react/components/fullscreen/demos/portal/css-modules/index.tsx new file mode 100644 index 00000000000..e1b1fe315a5 --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/portal/css-modules/index.tsx @@ -0,0 +1,40 @@ +import * as React from 'react'; +import { Fullscreen } from '@base-ui/react/fullscreen'; +import styles from './index.module.css'; + +export default function ExampleFullscreenPortal() { + return ( + + + + Open fullscreen + + + +

Mounted only when open

+

This content is only mounted while in fullscreen.

+ + + Close + +
+
+
+ ); +} + +function ExpandIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} + +function CloseIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/portal/index.ts b/docs/src/app/(docs)/react/components/fullscreen/demos/portal/index.ts new file mode 100644 index 00000000000..07771a187d8 --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/portal/index.ts @@ -0,0 +1,8 @@ +import { createDemoWithVariants } from 'docs/src/utils/createDemo'; +import CssModules from './css-modules'; +import Tailwind from './tailwind'; + +export const DemoFullscreenPortal = createDemoWithVariants(import.meta.url, { + CssModules, + Tailwind, +}); diff --git a/docs/src/app/(docs)/react/components/fullscreen/demos/portal/tailwind/index.tsx b/docs/src/app/(docs)/react/components/fullscreen/demos/portal/tailwind/index.tsx new file mode 100644 index 00000000000..d3845819488 --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/demos/portal/tailwind/index.tsx @@ -0,0 +1,43 @@ +import * as React from 'react'; +import { Fullscreen } from '@base-ui/react/fullscreen'; + +export default function ExampleFullscreenPortal() { + return ( + + + + Open fullscreen + + + +

+ Mounted only when open +

+

+ This content is only mounted while in fullscreen. +

+ + + Close + +
+
+
+ ); +} + +function ExpandIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} + +function CloseIcon(props: React.ComponentProps<'svg'>) { + return ( + + ); +} diff --git a/docs/src/app/(docs)/react/components/fullscreen/page.mdx b/docs/src/app/(docs)/react/components/fullscreen/page.mdx new file mode 100644 index 00000000000..22107678133 --- /dev/null +++ b/docs/src/app/(docs)/react/components/fullscreen/page.mdx @@ -0,0 +1,265 @@ +# Fullscreen + +Presents an element in fullscreen using the browser Fullscreen API. + + +import { DemoFullscreenHero } from './demos/hero'; + + + +## Usage guidelines + +- **User gesture required:** Browsers only honor `requestFullscreen()` while the window has [transient activation](https://html.spec.whatwg.org/multipage/interaction.html#transient-activation) — granted by a recent user interaction (click, tap, keypress) and valid for roughly five seconds. The bundled `` always satisfies this. Flipping `open` from any external handler triggered by a user gesture (including a deferred `setTimeout` that still runs within the activation window) also works. Requests fired without a recent gesture are rejected. +- **Browser support:** Fullscreen is unavailable on iOS Safari for iPhone (only iPad iOS 16.4+ supports `requestFullscreen()` on arbitrary elements). When the API is unavailable for the container's owner document, `` is automatically disabled (`data-disabled`, `aria-disabled="true"`). The `supported` state is also exposed on Root, Trigger, Container, and Close so you can render fallback UI. +- **Inside iframes:** The hosting `