Explore everything FotoGrids can do
Style every gallery to your brand
Drag, drop, publish. No code
Rank higher. Load lighter
Your photos stay your photos
Track. Engage. Grow
Native block for the WordPress editor
Dedicated widget with full control
Custom module for any Divi layout
Fully customizable native element
Anywhere WordPress renders
Read, write, and automate galleries
Customize behavior with WP hooks
Bring your own styles and scripts
Hooks, REST API, and full control
Showcase your work beautifully
Collaborate, deliver, and scale client work
Beautiful galleries without writing code
Turn visitors into paying customers
Product galleries built to sell
Creative portfolios for every field
Curated video playlists and embeds
Conferences, weddings and celebrations
E-Commerce-ready product showcases
Property photos that close deals
Client proofing with secure galleries
Every FotoGrids render ships a unique DOM ID, 200+ frontend classes, 300+ data-fg-* attributes and roughly 600+ CSS custom properties, about half of them emitted per Gallery instance. All of it is in Free, and none of it needs a plugin setting.
Not a simplified diagram — the markup below is what FotoGrids writes for a masonry Gallery with one item. Every hook you need to style is visible in the following lines.
fg-{collectionId}-{n}, where n is a 1-based per-request counter. Implement the same Gallery on a page twice and you get #fg-42-1 and #fg-42-2 — two addressable instances of one Gallery. CSS fotogrids-targeting.css CSS
/* SCOPE — how wide the rule reaches */
/* 1 — every Gallery and Album on the site */
.fotogrids-collection { }
/* 2 — every render of Gallery 42, wherever it appears */
[data-fg-gallery-id="42"] { }
/* 3 — one instance on one page */
#fg-42-1 { }
/* CONFIGURATION — narrow by how it is set up */
/* every masonry Gallery, at any scope above */
.fotogrids-collection[data-fg-layout="masonry"] { }
/* the frame before the image paints */
.fg-item[data-fg-media-state="loading"] { }
Every wrapper carries the umbrella class fotogrids-collection plus exactly one discriminator — fotogrids-gallery or fotogrids-album. Nothing else about the render is expressed as a class. All decorator and feature state is written as data-fg-* attributes, and data-fg-gallery-id is always written first.
That split is the whole design. Classes tell you what kind of thing an element is. Attributes tell you how it is configured. You style the first and read the second.
Level 2 is the one most people want and miss. #fg-42-1 is precise but instance-bound; [data-fg-gallery-id="42"] follows Gallery 42 onto every page it appears on, in every builder.
Scope and configuration compose. Put an attribute on a scope selector and you get the intersection — #fg-42-1[data-fg-layout="masonry"], or every masonry Gallery on the site, or only the ones inside a particular Album.
.fg-* classes and 250+ data-fg-* attributes ship on the frontend. These are the ones worth memorising. #fg-42-1
fg-{collectionId}-{n}, n a 1-based per-request counter. .fotogrids-collection The umbrella class on every Gallery and Album wrapper.
.fotogrids-gallery .fotogrids-album #fotogrids-view
.fotogrids-view--gallery View Page body class.
Also --album, --theme-dark, --theme-light.
.fg-item & .fg-item-media The item figure and its media box.
Plus .fg-overlay, .fg-item-loader, .fg-item-hidden.
.fg-caption-title & .fg-caption-description .fg-caption-content and .fg-caption-bg. .fg-masonry-track .fg-grid-track, .fg-justified-track, .fg-carousel-track, .fg-instant-photos-track, .fg-single-item-track, .fg-viewer-track, plus .fg-featured-grid for Featured Item, all inside .fg-layout-body — except Single Item and Slider, which opt out of pagination and so render their track without that wrapper. .fg-pagination--load-more --pages, --endless-scroll, --exhausted. .fg-is-open
fg-is-active, fg-is-closing, fg-is-loading, fg-is-zooming, fg-is-swapping, fg-is-leaving, fg-is-source, fg-is-trimmed. .fg-lb-grid-tile, .fg-filter-btn & .fg-gate-card [data-fg-gallery-id] & [data-fg-album-id] [data-fg-layout]
[data-fg-media-state] loading, then loaded. Style the placeholder frame off this. [data-fg-item-id] & [data-fg-item-type] [data-fg-item-id] — on the <a> that wraps each item, added by the Lightbox, external-link and direct-link decorators. [data-fg-item-type] — on the .fg-video element inside video items. [data-fg-natural-ratio="1"] [data-fg-page-current] & [data-fg-page-total] data-fg-page-size, data-fg-paginated and data-fg-total-items. [data-fg-hover] & [data-fg-loaded-effect] data-fg-lightbox-variant, data-fg-columns-mode, data-fg-restricted, data-fg-scope. --fg-cols
--fg-col-min --fg-col-max --fg-gap --fg-item-aspect-ratio --fg-item-fit --fg-radius --fg-border-w, --fg-border-color & --fg-border-style --fg-padding & --fg-margin --fg-justified-row-height --fg-featured-grid-cols & --fg-featured-grid-rows --fg-items-per-view --fg-height-fixed & --fg-height-max --fg-view-accent & --fg-view-max-width --fg-view-bg, --fg-view-fg, --fg-view-border & --fg-view-muted #fg-{collectionId}-{n}. Override any of them from your own stylesheet at whichever of the three targeting levels fits. --fg-caption-title-color .fg-caption-title. --fg-caption-title-font-size --fg-caption-title-lines --fg-caption-desc-color .fg-caption-description. --fg-caption-desc-font-size --fg-caption-desc-lines --fg-caption-overlay-bg .fg-caption-bg. Takes a gradient. --fg-caption-overlay-bg-hover --fg-caption-align --fg-caption-gap --fg-caption-padding --fg-hover-duration & --fg-hover-ease --fg-hover-zoom-scale --fg-hover-lift-distance --fg-hover-tilt-angle --fg-hover-tint-color --fg-hover-frame-color --fg-hover-filter --fg-hover-cursor & --fg-hover-cursor-link --fg-shadow --fg-shadow-x, --fg-shadow-y & --fg-shadow-blur --fg-shadow-hover --fg-elev-shadow-* --fg-tt-bg & --fg-tt-color --fg-tt-radius, --fg-tt-arrow-size & --fg-tt-z --fg-lb-bg --fg-lb-z --fg-lb-toolbar-* .fg-lb-grid-toolbar. --fg-lb-thumb-* --fg-lb-nav-* .fg-lb-grid-nav. --fg-lb-info-* --fg-lb-color-* --fg-lb-grid-* .fg-lb-grid-tile, .fg-lb-grid-stage, .fg-lb-grid-counter. --fg-lb-mv-* .fg-lb-mv-* classes. --fg-arrow-* --fg-bullet-* --fg-thumbs-spacing --fg-viewer-* --fg-pagination-btn-bg -hover-, -active- and -focus- variants. --fg-pagination-btn-color --fg-pagination-btn-border-color --fg-pagination-btn-radius & --fg-pagination-btn-padding --fg-pagination-gap & --fg-pagination-distance --fg-filter-btn-* .fg-filter-btn. --fg-filter-cb-* .fg-filter-checkbox. --fg-filter-dropdown-* & --fg-filter-select-* .fg-filter-dropdown-trigger. --fg-filter-count-* .fg-filter-count. --fg-filter-sidebar-width CSS emitted for one render of Gallery 42 CSS
#fg-42-1 {
--fg-radius: 8px 4px;
--fg-gap: 12px;
}
@media (max-width: 1024px) { #fg-42-1 { --fg-gap: 8px; } }
@media (max-width: 767px) { #fg-42-1 { --fg-gap: 6px; } }
/* --fg-radius is not repeated: its tablet and mobile
values match desktop, so nothing is emitted for it. */
--fg-gap changes at both breakpoints, so it appears three times. --fg-radius is identical at every size, so it appears once. That is the whole rule — the render layer compares each value against the level above and writes the override only on a difference.
The breakpoints are your site's own, not fixed constants. mobile_breakpoint defaults to 767 and tablet_breakpoint to 1024; both are configurable, and the filter fotogrids/render/breakpoint_config changes what the media queries use at render time.
Per-render CSS is enqueued, not embedded. It lands in <head> as an ordinary stylesheet, so a theme stylesheet enqueued after it — or any rule of your own on #fg-42-1 — wins on normal specificity and source order.
There are no inline style attributes on the wrapper and no !important on the variables to work around.
CSS recipe-01.css CSS
#fg-42-1 {
--fg-gap: 4px;
--fg-radius: 0;
}
Instance-scoped. Every other render on the site — including the second instance of Gallery 42 on the same page — is untouched.
CSS recipe-02.css
CSS
.fotogrids-collection {
--fg-caption-overlay-bg:
linear-gradient(transparent, rgba(0,0,0,.5));
--fg-caption-title-color: #fff;
--fg-caption-title-lines: 1;
--fg-caption-align: left;
}
CSS recipe-03.css
CSS
.fotogrids-collection[data-fg-layout="justified"] {
--fg-justified-row-height: 260px;
}
.fotogrids-collection[data-fg-layout="justified"]
.fg-item[data-fg-media-state="loading"] {
background: #eef0f7;
}
data-fg-media-state gives you a placeholder colour that disappears the moment the image loads. JS recipe-04.js
JS
document.addEventListener( 'fotogrids:gallery_initialized', ( e ) => {
const { galleryElement, galleryId } = e.detail;
galleryElement.querySelectorAll( '.fg-item' ).forEach( ( item, i ) => {
item.style.setProperty( '--stagger', `${ i * 40 }ms` );
} );
} );
A native CustomEvent on document.
No wp.hooks, no jQuery, no build step.
fotogrids-inline-{n} and attaches its CSS with wp_add_inline_style(), so FotoGrids’ variables land in <head> as an ordinary stylesheet and a theme sheet enqueued after it wins on the normal cascade. No inline styles on the wrapper, no !important to work around. PHP functions.php PHP
add_action( 'wp_enqueue_scripts', function () {
wp_enqueue_style(
'my-gallery-overrides',
get_stylesheet_directory_uri() . '/fotogrids.css',
array( 'fotogrids-render-base' ), // load after FotoGrids' base
'1.0.0'
);
}, 20 );
fotogrids-runtime is the dependency root for nearly all FotoGrids JavaScript. fotogrids-render-base ships collection-base.css and every layout requires it.
Handles compose the way you would expect: fotogrids-image-zoom depends on fotogrids-lightbox-mini, and fotogrids-video-lightbox-mini depends on both fotogrids-runtime and fotogrids-lightbox-mini.
Depend on one, replace one, or dequeue one.
Drop fotogrids/single-fotogrids_gallery.php, fotogrids/single-fotogrids_album.php or the fotogrids/single.php fallback into your theme.
The filter fotogrids/view/template resolves the path.
The shell renders under <body id="fotogrids-view"> with .fotogrids-view__header, __body, __footer and __notice.
Add classes with fotogrids/render/wrapper_css_classes, mutate the variable map before it is serialised with fotogrids/render/layout/style_vars, change the attributes with fotogrids/render/layout/wrapper_attrs, and move the breakpoints with fotogrids/render/breakpoint_config.
Also fotogrids/render/should_inline_assets, fotogrids/render/late_assets and fotogrids/view/body_classes.
The full list lives on Hooks & Filters.
fotogrids:ready fires on document. fotogrids:gallery_initialized carries { galleryElement, galleryId, kind, instance }.
Then fotogrids:lightbox:open, :close, :navigate, plus fotogrids:page_changed, fotogrids:items_inserted, fotogrids:filters_changed and fotogrids:share.
Standard CustomEvents throughout.
No. Every id, class, attribute and custom property on this page is in Free. The Pro feature is an editor inside the product for keeping the code — the styling surface itself is open now.
Use the instance id — #fg-42-1, where 42 is the collection id and the suffix is a 1-based per-request counter. Two instances of the same Gallery on one page get their own ids. Use [data-fg-gallery-id="42"] when you want every render of that Gallery wherever it appears.
Not on specificity. Per-render CSS is enqueued as an ordinary stylesheet through wp_add_inline_style(), not written inline on the wrapper, and the variables carry no !important. A theme sheet enqueued after fotogrids-render-base, or any #fg-… rule of your own, wins normally.
The standalone View Page shell, yes — fotogrids/single-fotogrids_gallery.php, single-fotogrids_album.php or single.php in your theme, resolved through fotogrids/view/template. That is the shell only, and only in standalone mode. Item and Gallery markup is built in PHP, so it changes through hooks.
Listen for fotogrids:gallery_initialized on document. The detail carries galleryElement, galleryId, kind and instance. For content that arrives later, fotogrids:items_inserted and fotogrids:page_changed fire on the Gallery element.
Install Free, drop a Gallery on a page, and every id, class, attribute and custom property on this page is in your DOM inside a minute.
We use cookies and similar technologies to make our website work smoothly and understand how it’s used. Choosing “Accept” helps us personalize your experience. If you decline or change your mind later, some features may not work as expected.