When creating content in Grav, you often need to display different types of media like images, videos, and various other files. These files are automatically found and processed by Grav and are made available to use by any page. This is particularly handy because you can then use the built-in functionality of the page to leverage thumbnails, access metadata and modify the media dynamically (e.g. resizing images, setting the display size for videos, etc.) as you need them.

Grav uses a smart-caching system that automatically creates in-cache copies of the dynamically generated media when necessary. This way all subsequent requests can use the cached version instead of having to generate the media all over again.

Supported Media Files

The following media file types are supported natively by Grav. Additional support for media files and streaming embeds may be added via plugins.

Media Type File Type
Image jpg, jpeg, png
Audio mp3, wav, wma, ogg, m4a, aiff, aif
Animated image gif
Vectorized image svg
Video mp4, mov, m4v, swf, flv, webm, ogv
Data / Information txt, doc, docx, html, htm, pdf, zip, gz, 7z, tar, css, js, json, xml, xls, xlt, xlm, xlsm, xld, xla, xlc, xlw, xll, ppt, pps, rtf, bmp, tiff, mpeg, mpg, mpe, avi, wmv

A full list of supported mimetypes can be found in the system/config/media.yaml file. If there is a mimetype that is not currently supported, you can simply create your own user/config/media.yaml and add it in there. Just ensure you follow the same format as the original system file. The simplest approach is to copy the whole original file and make your edits.

Where to put your media files

Usually you'll use a media file within a page, so just put the file in the page folder, and you can reference it in the Markdown of the page, for example:


If you want to put all your images in a single folder, you can put them in a user/pages/images folder. That way you can reach them via


and also you can find them easily via markdown and perform operations on them:


Alternatively you can put them in your theme, as that is easily accessible via CSS references.

Grav has a /images folder. Do not put your own images in that folder, as it hosts Grav auto-generated, cached images.

Display modes

Grav provides a few different display modes for every kind of media object.

Mode Explanation
source Visual representation of the media itself, i.e. the actual image, video or file
text Textual representation of the media
thumbnail The thumbnail image for this media object

Data / Information type media do not support source mode, they will default to text mode if another mode is not explicitly chosen.

Thumbnail Location

There are three locations Grav will look for your thumbnail.

  1. In the same folder as your media file: [media-name].[media-extension].thumb.[thumb-extension] where media-name and media-extension are respectively the name and extension of the original media file and thumb-extension is any extension that is supported by the image media type. Examples are my_video.mp4.thumb.jpg and my-image.jpg.thumb.png For images only! The image itself will be used as thumbnail.
  2. Your user folder: user/images/media/thumb-[media-extension].png where media-extension is the extension of the original media file. Examples are thumb-mp4.png and thumb-jpg.jpg
  3. The system folder: system/images/media/thumb-[media-extension].png where media-extension is the extension of the original media file. The thumbnails in the system folders are pre-provided by Grav.

You can also manually select the desired thumbnail with the actions explained below.

Links and Lightboxes

The display modes above can also be used in combination with links and lightboxes, which are explained in more detail later. Important to note however is:

Grav does not provide lightbox-functionality out of the box, you need a plugin for this. You can use the FeatherLight Grav plugin to achieve this.

When you use Grav's media functionality to render a lightbox, all Grav does is output an anchor tag that has some attributes for the lightbox plugin to read. If you are interested in using a lightbox library that is not in our plugin repository or you want to create your own plugin, you can use the table below as a reference.

Attribute Explanation
rel A simple indicator that this is not a regular link, but a lightbox link. The value will always be lightbox
href A URL to the media object itself
data-width The width the user requested this lightbox to be
data-height The height the user requested this lightbox to be
data-srcset In case of image media, this contains the srcset string. (more info)


Grav employs a builder-pattern when handling media, so you can perform multiple actions on a particular medium. Some actions are available for every kind of medium while others are specific to the medium.


These actions are available for all media types.


This method is only intended to be used in Twig templates, hence the lack of Markdown syntax.

This returns raw url path to the media.

{{['sample-image.jpg'].url }}

html([title][, alt][, classes])

In Markdown this method is implicitly called when using the ![] syntax.

The html action will output a valid HTML tag for the media based on the current display mode.

![Some ALT text](sample-image.jpg "My title") {.myclass}
{{['sample-image.jpg'].html('My title', 'Some ALT text', 'myclass') }}
<img title="My title" alt="Some ALT text" class="myclass" src="/images/3/d/b/e/d/3dbed3583a26e9ab2fd26220a554f0896c273352-sample-image.jpeg" />
some ALT text

To use classes in Markdown, you need to enable Markdown Extra.


Use this action to switch between the various display modes that Grav provides. Once you switch display mode, all previous actions will be reset. The exceptions to this rule are the lightbox and link actions and any actions that have been used before those two.

For example, the thumbnail that results from calling['sample-image.jpg'].sepia().display('thumbnail').html() will not have the sepia() action applied, but['sample-image.jpg'].display('thumbnail').sepia().html() will.

Once you switch to thumbnail mode, you will be manipulating an image. This means that even if your current media is a video, you can use all the image-type actions on the thumbnail.


Turn your media object into a link. All actions that you call before link() will be applied to the target of the link, while any actions called after will apply to what's displayed on your page.

After calling link(), Grav will automatically switch the display mode to thumbnail.

The following example will display a textual link (display('text')) to a sepia version of the sample-image.jpg file:

![Image link](sample-image.jpg?sepia&link&display=text)
{{['sample-image.jpg'].sepia().link().display('text').html('Image link') }}
<a href="/images/f/4/9/f/8/f49f81917528ee45bba7b67fdc79faa1851fa255-sample-image.jpeg"><p title="Image link">Image link</p></a>

Image link

lightbox([width, height])

The lightbox action is essentially the same as the link action but with a few extras. Like explained above, the lightbox action will not do anything more than create a link with some extra attributes. It differs from the link action in that it adds a rel="lightbox" attribute and accepts a width and height attribute.

If possible (currently only in the case of images), Grav will resize your media to the requested width and height. Otherwise it will simply add a data-width and data-height attribute to the link.

![Sample Image](sample-image.jpg?lightbox=600,400&resize=200,200)
{{['sample-image.jpg'].lightbox(600,400).resize(200,200).html('Sample Image') }}
<a rel="lightbox" data-width="600" data-height="400" href="/images/f/0/e/a/2/f0ea27a81df7952cab63503e42b7ac4c0b75b8fe-sample-image.jpeg"><img title="Sample Image" src="/images/9/4/8/2/e/9482e46ef1a397a00fbd02b539968ed96762204f-sample-image.jpeg" /></a>

Sample Image


Manually choose the thumbnail Grav should use. You can choose between page and default for any type of media as well as media for image media if you want to use the media object itself as your thumbnail.

![Sample Image](sample-image.jpg?thumbnail=default&display=thumbnail)
{{['sample-image.jpg'].thumbnail('default').display('thumbnail').html('Sample Image') }}
<img title="Sample Image" src="/system/images/media/thumb-jpg.png" />

Sample Image

Image Actions

resize(width, height, [background])

Resizing does exactly what you would expect it to do. resize lets you create a new image based on the width and the height. The aspect ratio is maintained and the new image will contain blank areas in the color of the optional background color provided as a hex value, e.g. 0xffffff. The background parameter is optional, and if not provided will default to transparent if the image is a PNG, or white if it is a JPEG.

![Sample Image](sample-image.jpg?resize=400,200)
{{['sample-image.jpg'].resize(400, 200).html() }}

Sample Image

forceResize(width, height)

Resizes the image to the width and height as provided. forceResize will not respect original aspect-ratio and will stretch the image as needed to fit the new image size.

![Sample Image](sample-image.jpg?forceResize=200,300)
{{['sample-image.jpg'].forceResize(200, 300).html() }}

Sample Image

cropResize(width, height)

cropResize resizes an image to a smaller or larger size based on the width and the height. The aspect ratio is maintained and the new image will be resized to fit in the bounding-box as described by the width and height provided. In other words, any background area you would see in a regular resize is cropped.

For example, if you have an image that is 640 x 480 and you perform a cropResize(100, 100) action upon it, you will end up with an image that is 100 x 75.

![Sample Image](sample-image.jpg?cropResize=300,300)
{{['sample-image.jpg'].cropResize(300, 300).html() }}

Sample Image

crop(x, y, width, height)

crop will not resize the image at all, it will merely crop the original image so that only the portion of the bounding box as described by the width and the height originating from the x and y location is used to create the new image.

For example, an image that is 640 x 480 that has the crop(0, 0, 400, 100) action upon it, will simply get the width and height both cropped so that the resulting image is an image with a width of 400 and a height of 100 originated from the top-left corner as described by 0, 0.

![Sample Image](sample-image.jpg?crop=100,100,300,200)
{{['sample-image.jpg'].crop(100,100,300,200).html() }}

Sample Image

cropZoom(width, height)

Similar to regular cropResize, cropZoom also takes a width and a height but will resize and crop the image to ensure the resulting image is the exact size you requested. The aspect ratio is maintained but parts of the image may be cropped, however the resulting image is centered.

The primary difference between cropResize and cropZoom is that in cropResize, the image is resized maintaining aspect ratio so that the entire image is shown, and any extra space is considered background.

With cropZoom, the image is resized so that there is no background visible, and the extra image area of the image outside of the new image size is cropped.

For example if you have an image that is 640 x 480 and you perform a cropZoom(400, 100) action, the resulting image will be resized to 400 x 300 and then the height is cropped resulting in a 400 x 100 image.

![Sample Image](sample-image.jpg?cropZoom=600,200)
{{['sample-image.jpg'].cropZoom(600,200).html() }}

Folks familiar with using zoomCrop for this purpose will find that it also works in Grav.


Sample Image


Dynamically allows the setting of a compression percentage value for the image between 0 and 100. A lower number means less quality, where 100 means maximum quality.

![Sample Image](sample-image.jpg?cropZoom=300,200&quality=25)
{{['sample-image.jpg'].cropZoom(300,200).quality(25).html() }}

Sample Image


Applies a negative filter to the image where colors are inverted.

![Sample Image](sample-image.jpg?cropZoom=300,200&negate)
{{['sample-image.jpg'].cropZoom(300,200).negate.html() }}

Sample Image


Applies a brightness filter to the image with a value from -255 to +255. Larger negative numbers will make the image darker, while larger positive numbers will make the image brighter.

![Sample Image](sample-image.jpg?cropZoom=300,200&brightness=-100)
{{['sample-image.jpg'].cropZoom(300,200).brightness(-100).html() }}

Sample Image


This applies a contrast filter to the image with a value from -100 to +100. Larger negative numbers will increase the contrast, while larger positive numbers will reduce the contrast.

![Sample Image](sample-image.jpg?cropZoom=300,200&contrast=-50)
{{['sample-image.jpg'].cropZoom(300,200).contrast(-50).html() }}

Sample Image


This processes the image with a grayscale filter.

![Sample Image](sample-image.jpg?cropZoom=300,200&grayscale)
{{['sample-image.jpg'].cropZoom(300,200).grayscale.html() }}

Sample Image


This processes the image with an embossing filter.

![Sample Image](sample-image.jpg?cropZoom=300,200&emboss)
{{['sample-image.jpg'].cropZoom(300,200).emboss.html() }}

Sample Image


This applies a smoothing filter to the image based on smooth value setting from -10 to 10.

![Sample Image](sample-image.jpg?cropZoom=300,200&smooth=5)
{{['sample-image.jpg'].cropZoom(300,200).smooth(5).html() }}

Sample Image


This applies a sharpening filter on the image.

![Sample Image](sample-image.jpg?cropZoom=300,200&sharp)
{{['sample-image.jpg'].cropZoom(300,200).sharp.html() }}

Sample Image


This applies an edge finding filter on the image.

![Sample Image](sample-image.jpg?cropZoom=300,200&edge)
{{['sample-image.jpg'].cropZoom(300,200).edge.html() }}

Sample Image

colorize(red, green, blue)

You can colorize the image based on adjusting the red, green, and blue values for the image from -255 to +255 for each color.

![Sample Image](sample-image.jpg?cropZoom=300,200&colorize=100,-100,40)
{{['sample-image.jpg'].cropZoom(300,200).colorize(100,-100,40).html() }}

Sample Image


This applies a sepia filter on the image to produce a vintage look.

![Sample Image](sample-image.jpg?cropZoom=300,200&sepia)
{{['sample-image.jpg'].cropZoom(300,200).sepia.html() }}

Sample Image


rotates the image by angle degrees counterclockwise, negative values rotate clockwise.

![Sample Image](sample-image.jpg?cropZoom=300,200&rotate=-90)
{{['sample-image.jpg'].cropZoom(300,200).rotate(-90).html() }}

Sample Image

flip(flipVertical, flipHorizontal)

flips the image in the given directions. Both params can be 0|1. Both 0 is equivalent to no flipping in either direction.

![Sample Image](sample-image.jpg?cropZoom=300,200&flip=0,1)
{{['sample-image.jpg'].cropZoom(300,200).flip(0,1).html() }}

Sample Image


Fixes the orientation of the image when rotation is made via EXIF data (applies to jpeg images taken with phones and cameras).

![Sample Image](sample-image.jpg?fixOrientation)
{{['sample-image.jpg'].fixOrientation }}

Animated / Vectorized Actions

resize(width, height)

Because PHP cannot handle dynamically resizing these types of media, the resize action will only make sure that a width and height or data-width and data-height attribute are set on your <img>/<video> or <a> tag respectively. This means your image or video will be displayed in the requested size, but the actual image or video file will not be converted in any way.

{{[''].resize(400, 200).html() }}
<video controls="1" style="width: 400px;height: 200px;"><source src="/user/pages/02.content/">Your browser does not support the video tag.</video>

Some examples of this:




Audio Actions

Audio media will display an HTML5 audio link:

![Hal 9000: I'm Sorry Dave](hal9000.mp3)
{{['hal9000.mp3'].html() }}

File Actions

Grav does not provide any custom actions on files at this point in time and there are no plans to add any. Should you think of something, please contact us.

[View Text File](acronyms.txt)
<a href="{{['acronyms.txt'].url() }}">View Text File</a>

View Text File


As you can see: Grav provides some powerful image manipulation functionality that makes it really easy to work with images! The real power comes when you combine multiple effects and produce some very sophisticated dynamic image manipulations. For example, this is totally valid:

![Sample Image](sample-image.jpg?negate&lightbox&cropZoom=200,200)
{{['sample-image.jpg'].negate.lightbox.cropZoom(200,200) }}

Sample Image

Responsive images

Higher density displays

Grav has built-in support for responsive images for higher density displays (e.g. Retina screens). Grav accomplishes this by implementing srcset from the Picture element HTML proposal. A good article to read if you want to understand this better is this blog post by Eric Portis.

Grav sets the sizes argument mentioned in the posts above to full viewport width by default. Use the sizes action showcased below to choose yourself.

To start using responsive images, all you need to do is add higher density images to your pages by adding a suffix to the file name. If you only provide higher density images, Grav will automatically generate lower quality versions for you. Naming works as follows: [image-name]@[density-ratio]x.[image-extension], so for example adding sample-image@3x.jpg to your page will result in Grav creating a 2x and a 1x (regular size) version by default.

These files generated by Grav will be stored in the images/ cache folder, not your page folder.

![Retina Image](retina.jpg?sizes=80vw)
{{['retina.jpg'].sizes('80vw').html() }}
<p><img alt="Retina Image" src="/images/8/7/3/e/1/873e1fecf30cbec18f0fd260f5c76f9214ee2fda-retina1x.jpeg" srcset="/images/5/5/6/e/5/556e57e2f9de3ba73a148e60dd865c264d4eadea-retina2x.jpeg 2880w, /images/8/7/3/e/1/873e1fecf30cbec18f0fd260f5c76f9214ee2fda-retina1x.jpeg 1440w" sizes="80vw" /></p>

Retina Image

Depending on your display and your browser's implementation and support for srcset, you might never see a difference. We included the HTML markup in the third tab so you can see what's happening behind the screens.

Sizes with media queries

Grav also has support for media queries inside the sizes attribute, allowing you to use different widths depending on the device's screen size. In contrast to the first method, you don't have to create multiple images; they will get created automatically. The fallback image is the current image, so a browser without support for srcset, will display the original image.

For the moment it does not work inside markdown, only in your twig files.

{{['retina.jpg'].sizes('(max-width:26em) 100vw, 50vw').html() }}
<p><img alt="" src="/images/8/7/3/e/1/873e1fecf30cbec18f0fd260f5c76f9214ee2fda-retina1x.jpeg" srcset="/images/5/5/6/e/5/556e57e2f9de3ba73a148e60dd865c264d4eadea-retina2x.jpeg 2880w, /images/8/7/3/e/1/873e1fecf30cbec18f0fd260f5c76f9214ee2fda-retina1x.jpeg 1440w" sizes="(max-width:26em) 100vw" /></p>

Depending on your display and your browser's implementation and support for srcset, you might never see a difference. We included the HTML markup in the fourth tab so you can see what's happening behind the screens.

Sizes with media queries using derivatives

If you want to customize the sizes of the automatically created files, you can use the derivatives() method (as shown below). The first parameter is the width of the smallest of the generated images. The second is the maximum width of the generated images. The third, and only optional parameter, dictates the intervals with which to generate the photos (default is 200). For example, if you set the first parameter to be 320 and the third to be 100, Grav will generate an image for 320, 420, 520, 620, and so on until it reaches its set maximum.

In our example, we set the maximum to 1600. This will result in increments of 300 being met from 320 to 1520 as 1620 would be above the threshold.

For the moment it does not work inside markdown, only in your twig files.

{{['retina.jpg'].derivatives(320,1600,300).sizes('(max-width:26em) 100vw, 50vw').html() }}
<p><img alt="" src="/images/8/7/3/e/1/873e1fecf30cbec18f0fd260f5c76f9214ee2fda-retina1x.jpeg" srcset="/images/5/5/6/e/5/556e57e2f9de3ba73a148e60dd865c264d4eadea-retina2x.jpeg 2880w, /images/4/7/6/1/f/4761fc7337b10bc06ea73ad4e1b145790afb3697-retina320w.jpeg 320w, /images/b/c/a/e/7/bcae77ab7cb3decb5db09ea5921654987c8ff294-retina620w.jpeg 620w, /images/d/2/c/a/a/d2caa040a4dc3125e2f5f76ae0a1b8021ed3a310-retina920w.jpeg 920w, /images/3/7/4/a/f/374af262e3f7743554afebf023dcc117570b8edc-retina1220w.jpeg 1220w, /images/9/5/7/6/f/9576fc1c2349fd9e1549085c9af62ff7a3398279-retina1520w.jpeg 1520w, /images/8/7/3/e/1/873e1fecf30cbec18f0fd260f5c76f9214ee2fda-retina1x.jpeg 1440w" sizes="(max-width:26em) 100vw" /></p>

Depending on your display and your browser's implementation and support for srcset, you might never see a difference. We included the HTML markup in the fourth tab so you can see what's happening behind the screens.


Every medium that you reference in Grav, e.g. image1.jpg,, or even has the ability to have variables set or even overridden via a metafile. These files take the format of <filename>.meta.yaml. For example, for an image with the filename image1.jpg you could create a metafile called image1.jpg.meta.yaml.

You can add just about any setting or piece of metadata you would like using this method.

The contents of this file should be in YAML syntax, an example could be:

            - [cropResize, 300, 300]
            - sharp
alt_text: My Alt Text

If you are using this method to add file-specific styling or meta tags for a single file, you will want to put the YAML file in the same folder as the referenced file. This will ensure that the file is pulled along with the YAML data. It's a handy way to even set file-specific metadata as you are unable to do so from the page itself.

Let's say you wanted to just pull the alt_text value listed for the image file sample-image.jpg. You would then create a file called sample-image.jpg.meta.yaml and place it in the folder with the referenced image file. Then, insert the data used in the example above and save that YAML file. In the markdown file for the page, you can display this data by using the following line:

{{['sample-image.jpg'].meta.alt_text }}

This will pull up the example phrase My Alt Text instead of the image. This is just a basic example. You can use this method for a number of things, including creating a gallery with multiple unique data points you want to have referenced for each image. Your images, in essence, have a set of data unique to them that can be easily referenced and pulled as needed.