Lightbox
componentAn image enlarged in a modal dialog: its thumbnail in a button that opens the native dialog by the invokers of HTML, command="show-modal" and commandfor; Escape, its close button and a click on its backdrop, closedby="any", close it, and the browser gives the focus back to the thumbnail. Its Stimulus controller, lightbox, stands in only where the browser runs neither the invokers nor closedby.
Preview
Code
<twig:Lightbox id="bridge" src="/images/bridge-480.jpg" alt="A bridge at dusk" :width="480" :height="360" :full="{src: '/images/bridge.jpg', width: 1600, height: 1200}" openLabel="Enlarge" />
Code
<twig:Lightbox id="bridge" src="/images/bridge-480.jpg" alt="A bridge at dusk" :width="480" :height="360" :full="{src: '/images/bridge.jpg', width: 1600, height: 1200}" openLabel="Enlarge" />
Usage
Code
<twig:Lightbox
id="bridge"
src="{{ asset('images/bridge-480.jpg') }}"
alt="A bridge at dusk"
:width="480"
:height="360"
:full="{src: asset('images/bridge.jpg'), width: 1600, height: 1200}"
openLabel="{{ 'image.enlarge'|trans }}"
closeLabel="{{ 'dialog.close'|trans }}"
/>
Installation
php bin/console ux:install lightbox --kit=github.com/parler-haut-interagir-librement/ux-kit
It brings the recipes it needs: image
Props
| Prop | Type | Default | Description |
|---|---|---|---|
srcset |
string|array<int|string,string>|null |
null |
The candidates of the thumbnail as HTML writes them, or a map of densities, or of widths with sizes, to URLs. |
sizes |
string|null |
null |
The sizes of the thumbnail's candidates of widths. |
sources |
— |
[] |
list<array{type?:string,srcset:string|array<int|string,string>,sizes?:string,media?:string,width?:int,height?:int}> The sources of the thumbnail, as for Picture. |
aspect |
string|null |
null |
The ratio of the thumbnail's box, as for Image. |
fit |
string|null |
null |
How the thumbnail fills its box, "cover" or "contain". |
position |
string|null |
null |
Where the thumbnail is cropped in its box. |
full |
— |
null |
array{src:string,width:int,height:int,srcset?:string|array<int|string,string>,sizes?:string,sources?:list<array<string,mixed>>}|null The full image shown in the dialog: the thumbnail itself when null; its candidates sized to the width of the screen unless it has sizes. |
caption |
string|null |
null |
A caption under the thumbnail and under the full image. |
credit |
string|null |
null |
Who made the image, as the site writes it, under the caption. |
license |
string|null |
null |
The name of the license of the image, under the caption. |
licenseHref |
string|null |
null |
The address of the license, which its name then links to with rel="license". |
creation |
string|BackedEnum|null |
null |
How the image was made, a code of the IPTC Digital Source Type vocabulary, or an enum whose value is one. |
creationLabel |
string|null |
null |
The words that say how the image was made; the name the vocabulary gives the code when null. |
openLabel |
string|null |
null |
Words screen readers read after the text alternative in the button, such as "Enlarge". |
closeLabel |
string |
'Close' |
What screen readers read for the close button. |
Examples
With a caption
The caption, the credit, the license and the creation show under the thumbnail and under the full image.
<twig:Lightbox id="map" src="/images/map-480.png" alt="The map of the city" :width="480" :height="360" caption="Where we meet" credit="Drawing: Ana Lima" />
Accessibility
The button is named by the image's text alternative, then by openLabel, read by screen readers alone; the dialog is
named by the text alternative, or by the caption when the image is decorative. The full image loads lazily, when the
dialog opens. The close button reads closeLabel. Every text is a prop, for the application to translate.
Files to copy
assets/controllers/lightbox_controller.js
import { Controller } from '@hotwired/stimulus';
/** Whether a click fell outside a box, on the backdrop of a modal dialog. */
export function isOutside({ clientX, clientY }, { top, right, bottom, left }) {
return clientX < left || clientX > right || clientY < top || clientY > bottom;
}
/** Whether the browser runs the `command` and `commandfor` of a button itself: the invokers of HTML. */
export function hasInvokers() {
return typeof HTMLButtonElement !== 'undefined' && 'commandForElement' in HTMLButtonElement.prototype;
}
/** Whether the browser closes a dialog on a click on its backdrop itself, as `closedby="any"` asks. */
export function hasLightDismiss() {
return typeof HTMLDialogElement !== 'undefined' && 'closedBy' in HTMLDialogElement.prototype;
}
/*
* The fallback of a lightbox. Its buttons carry `command` and `commandfor` and its dialog `closedby="any"`: where the
* browser runs them, the controller does nothing. Where it does not, the controller opens the dialog as a modal from
* the thumbnail, closes it from its button, and on a click on its backdrop.
*/
export default class extends Controller {
static targets = ['dialog'];
open() {
if (!hasInvokers() && !this.dialogTarget.open) {
this.dialogTarget.showModal();
}
}
close() {
if (!hasInvokers() && this.dialogTarget.open) {
this.dialogTarget.close();
}
}
closeOnClickOutside(event) {
if (!hasLightDismiss() && event.target === this.dialogTarget && isOutside(event, this.dialogTarget.getBoundingClientRect())) {
this.dialogTarget.close();
}
}
}
templates/components/Lightbox.html.twig
{#-
# An image enlarged in a modal dialog: its thumbnail in a button that opens the native dialog by the invokers of HTML,
# command="show-modal" and commandfor, the dialog closed by Escape, by its close button or by a click on its backdrop
# with closedby="any"; the lightbox controller stands in only where the browser runs neither. Both pictures go through
# the Picture of the image recipe, the full one shown whole; the caption, the credit, the license and the creation
# show under both. A refused image draws nothing.
#}
{%- props
## string The identifier of the lightbox, unique in the page, from which that of its dialog is made.
id,
## string The URL of the thumbnail.
src,
## string The text alternative, which also names the dialog; an empty string names it by the caption.
alt,
## int The width of the thumbnail, so that its space is reserved at its ratio before it loads.
width,
## int The height of the thumbnail, so that its space is reserved at its ratio before it loads.
height,
## string|array<int|string,string>|null The candidates of the thumbnail as HTML writes them, or a map of densities, or of widths with sizes, to URLs.
srcset = null,
## string|null The sizes of the thumbnail's candidates of widths.
sizes = null,
## list<array{type?:string,srcset:string|array<int|string,string>,sizes?:string,media?:string,width?:int,height?:int}> The sources of the thumbnail, as for Picture.
sources = [],
## string|null The ratio of the thumbnail's box, as for Image.
aspect = null,
## string|null How the thumbnail fills its box, "cover" or "contain".
fit = null,
## string|null Where the thumbnail is cropped in its box.
position = null,
## array{src:string,width:int,height:int,srcset?:string|array<int|string,string>,sizes?:string,sources?:list<array<string,mixed>>}|null The full image shown in the dialog: the thumbnail itself when null; its candidates sized to the width of the screen unless it has sizes.
full = null,
## string|null A caption under the thumbnail and under the full image.
caption = null,
## string|null Who made the image, as the site writes it, under the caption.
credit = null,
## string|null The name of the license of the image, under the caption.
license = null,
## string|null The address of the license, which its name then links to with rel="license".
licenseHref = null,
## string|BackedEnum|null How the image was made, a code of the IPTC Digital Source Type vocabulary, or an enum whose value is one.
creation = null,
## string|null The words that say how the image was made; the name the vocabulary gives the code when null.
creationLabel = null,
## string|null Words screen readers read after the text alternative in the button, such as "Enlarge".
openLabel = null,
## string What screen readers read for the close button.
closeLabel = 'Close'
-%}
{%- if image_origin(src) is not same as('refused') and not (src|replace({"\t": '', "\n": '', "\r": ''}) matches '#^[[:cntrl:] ]*(javascript:|vbscript:|data:(?!image/))#i') -%}
{%- set _dialog = 'lightbox-' ~ id -%}
{%- set _full = full ?? {src: src, srcset: srcset, sources: sources, width: width, height: height} -%}
<div {{ html_attr({class: ['isLightbox', attributes.all.class ?? null]|filter(name => name is not empty)|join(' ')}) }} data-controller="lightbox">
<twig:Image:Figure :caption="caption" :credit="credit" :license="license" :licenseHref="licenseHref" :creation="creation" :creationLabel="creationLabel">
<button type="button" class="isLightboxTrigger" command="show-modal" commandfor="{{ _dialog }}" aria-haspopup="dialog" data-action="click->lightbox#open">
<twig:Picture :src="src" :alt="alt" :width="width" :height="height" :srcset="srcset" :sizes="sizes" :sources="sources" :aspect="aspect" :fit="fit" :position="position" />
{%- if openLabel is not empty %}<span class="isVisuallyHidden">{{ openLabel }}</span>{% endif -%}
</button>
</twig:Image:Figure>
<dialog id="{{ _dialog }}" class="isLightboxDialog" closedby="any" aria-label="{{ alt is not empty ? alt : caption }}" data-lightbox-target="dialog" data-action="click->lightbox#closeOnClickOutside">
<twig:Picture :src="_full.src ?? src" :alt="alt" :width="_full.width ?? width" :height="_full.height ?? height" :srcset="_full.srcset ?? null" :sizes="_full.sizes ?? '100vw'" :sources="_full.sources ?? []" fit="contain" :caption="caption" :credit="credit" :license="license" :licenseHref="licenseHref" :creation="creation" :creationLabel="creationLabel" />
<button type="button" class="isLightboxClose" command="close" commandfor="{{ _dialog }}" data-action="click->lightbox#close"><span aria-hidden="true">×</span><span class="isVisuallyHidden">{{ closeLabel }}</span></button>
</dialog>
</div>
{%- endif -%}