Galleries & Resource Collections
Build media components around project resources, folders, and drag-and-drop
Media components live or die on how well they handle someone else's files. You don't know how many images the user will add, what shape they'll be, whether captions exist, or whether one of them turns out to be a video. Elements' resource system absorbs all of that variability before your template runs: the user hands your component a folder, your hooks turn it into a clean array of ready-to-render items, and your template just loops. This guide walks that whole pipeline through a real component, then extends it with patterns for albums, metadata, and switchable layouts.
This guide dissects the Gallery component (com.realmacsoftware.gallery) from the open-source Core Pack. Open its source alongside this page.
Resource Fundamentals, Briefly
What are Resources? covers the ground rules, so here is just the working summary. A resource is any file the user has added to their project — image, video, audio, or an entire folder. Your component asks for one by declaring a resource control in properties.json, and Elements delivers an object (not a bare URL) into rw.props and your templates: path, intrinsic dimensions, aspect ratio, alt text, format. Restrict what the control accepts with its accepts/excludes options if your component only handles certain types.
Two facts matter most for gallery-style components:
A folder is a single resource value. Assign a folder and the property becomes a container whose
resourcesfield is an array of resource objects — one property in the inspector, arbitrarily many files behind it. See Image Resources.Any element can be a drop target. Add
rwResourceDropZone="<control id>"to an element in your template — or to your root element from hooks — and users can drag files from Finder or the Resources browser straight onto your component on the canvas.
The Core Gallery, End to End
Gallery is the canonical resource-centric component: one folder in, a responsive thumbnail grid and a full-screen lightbox out. Everything it does flows from a single control in its properties.json — {"title": "Resources", "id": "resources", "resource": {}} — with no accepts restriction, because Gallery handles images and videos alike (you'll see how it tells them apart in a moment). The rest of its large inspector is pure presentation: columns, gap, aspect ratio, caption styling, lightbox chrome. The content side is this one dropwell.
The Empty State Is the Drop Target
The first thing a user sees after dropping Gallery on the canvas is nothing — no folder assigned yet. Gallery's main template branches on that case immediately, and makes the placeholder itself the drop zone:
<!-- com.realmacsoftware.gallery/templates/index.html -->
@if(!hasResources)
<div
rwResourceDropZone="resources"
class="col-span-full w-full h-64 flex flex-col items-center justify-center border-2 border-dashed border-gray-300 rounded-xl bg-gray-50 hover:bg-gray-100 transition cursor-pointer text-center px-4 py-6"
>
<!-- … folder icon SVG … -->
<p class="text-lg font-medium text-gray-700">
Drag & drop a folder of resources
</p>
<p class="text-sm text-gray-400">or add them via the component inspector</p>
</div>
@else
<div class="{{classes.wrapper}}" x-data>
@each(image in resources) @include("thumbnail") @endeach
</div>
@endif @includeIf(includeLightbox, template: "lightbox")The rwResourceDropZone="resources" value matches the resource control's id, so a folder dragged onto the dashed box lands in the resources property and the whole component re-renders as a grid. The instruction and the target are the same element — users are never told to drop somewhere they can't.
Gallery scopes its drop zone to the empty state, but a component that stays droppable after content is assigned can wire the attribute onto its root element from hooks instead. The core Image component does exactly that, passing it through rw.setRootElement's args:
With that in place, dragging a new image anywhere onto the component replaces the current one — no need to hunt for the inspector.
Processing the Folder in Hooks
Templates should loop and print, nothing more. All of Gallery's real work happens in hooks.source.js, which walks the folder's resources array once and enriches every item before the template sees it:
Four moves worth internalising:
hasResourcesis precomputed. Template@iftakes exactly one condition, so the length check lives here and the template tests a plain boolean. Every folder-driven component needs this line.Every item gets a thumbnail.
rw.resizeResource()generates a 400px-wide variant per image and attaches its path to the item asthumbnail. Resizing at render time is impossible — templates can only read — so hooks are where scaled variants are born. For the full retina/srcset/per-breakpoint treatment, see Responsive Images & Media.Alt text degrades gracefully.
alt || caption || author || ""means an image with no alt text still gets something descriptive, and the template never has to think about it.Format becomes booleans.
resource.formatdistinguishes videos (youtube,vimeo,mp4) from images, but rather than making the template compare strings — which@ifcan't do — the hook flattens the answer intoisVideo,isYouTube,isVimeo, andisMP4flags the template can test directly.
Finally, rw.setProps({ resources: resources?.resources }) promotes the inner array to a top-level prop, so the template writes @each(image in resources) instead of reaching through the container object.
Rendering the Thumbnails
Back in index.html, the populated branch is one line — @each(image in resources) @include("thumbnail") @endeach — with the per-item markup extracted to an include. Includes share the scope of the template that includes them, so thumbnail.html can use the image loop variable and the classes prop directly:
Notice src="{{image}}" in the image branch: printing a resource object directly outputs its file path, so this renders the original file. The hook's 400px variant is on the same item as {{image.thumbnail}} whenever you want the scaled file instead — attach variants in hooks and each template decides which to print. The {{image::index}} loop variable (see @each) rides along in the Alpine $dispatch calls, telling the lightbox which slide the clicked thumbnail corresponds to.
thumbnailWantsMeta is another precomputed boolean — the hook sets it to thumbnailShowCaption || thumbnailShowAuthor, an OR the template couldn't express itself.
The Lightbox, Gated for the Canvas
The last line of index.html — @includeIf(includeLightbox, template: "lightbox") — decides whether the lightbox exists at all. Look back at the hook: includeLightbox is lightboxPreview || rw.project.mode !== "edit". A fixed full-screen overlay would be hostile on the editing canvas, so by default it's only included outside edit mode; the inspector's Lightbox Preview switch opts back in so users can style it. The include itself teleports to the end of the page body and iterates the same processed array a second time:
The video booleans computed in hooks pay off again — each @includeIf swaps in a provider-specific embed template, and images fall through to a plain <img> sized by the resource's own aspect field. The full story of mode gating (and why x-cloak sits inside @if(!edit)) is in Designing the Edit-Mode Experience.
Albums from Nested Folders
A folder's resources array can contain folders. That one fact turns a flat gallery into an album browser: the user assigns a folder of folders, and each subfolder becomes an album with a name, a cover, and its own image set. A folder item carries a resources array of its own instead of an image field, which is exactly how a hook can tell the two apart. Here's a component we'll call com.example.albums, with a single resource control (id: "library"):
One pass builds everything the template needs: the first image in each subfolder becomes the album cover (resized to 600px), and every image gets an 800px display variant plus the same alt-fallback chain Gallery uses. The template renders a cover grid, then a detail section per album — the covers anchor-link down to their sections:
When hasAlbums is false, give the component the same rwResourceDropZone placeholder Gallery uses. The anchor-link detail view is deliberately the simplest sketch that works — to open albums in place instead, reuse the show/hide machinery from Designing the Edit-Mode Experience; the data shape doesn't change, only the markup around album.images. And because the hook filters out empty albums and non-folder items, stray loose files in the user's folder are simply ignored rather than breaking the layout.
Captions & Metadata
Every resource object carries a small, fixed set of descriptive fields. These are the ones documented in the references and used by the core Gallery — treat this as the complete menu:
image
Images
Path to the image file
path
All files
Path for non-image files (video, audio, PDF)
width / height
Images
Intrinsic pixel dimensions
aspect
Images
Ratio string such as 16/9 — drops straight into aspect-[…]
alt
Images
Alt text set in the Resources browser
format
All files
File format token, e.g. jpg, mp4, youtube
caption / author
Folder items
Per-item metadata on resources inside a folder
name
Folders & items
The folder's or file's name
Captions and authorship render like any other field — pair them in a <figure> and fall back gracefully when they're empty:
Gallery's alt chain — alt || caption || author || "" — is the pattern to copy: compute the best available description once in hooks so templates never render an image with a missing alt attribute.
That's the extent of what Elements hands you. Richer metadata — camera EXIF, GPS, capture dates — isn't part of the resource object, and the hooks runtime has no module system to pull in a parser at run time. If you need it, bundle an EXIF library into your compiled hooks.js at build time and read the file yourself; Integrating JavaScript Libraries covers the bundling workflow.
Switching Layout Engines
Once your hook owns all class generation (as Gallery's does with its classes object), offering multiple layout engines costs almost nothing: one inspector control, one branch in the hook, zero changes to the template. Here's a two-engine switcher — uniform grid versus CSS-columns masonry — driven by a Segmented control. responsive: false makes it return the raw item value rather than breakpoint-prefixed classes:
The template is engine-agnostic: it prints {{classes.wrapper}}, {{classes.item}}, and {{classes.image}} and never knows which engine is active. Adding a third engine — say a JavaScript-measured masonry for perfectly packed layouts — is another branch in the hook and another segment in the control, with the same markup underneath. That's the property that keeps big media components maintainable as their inspectors grow.
Related Documentation
Last updated
Was this helpful?

