How We Built Our Design System
Building a comprehensive design system requires careful planning and collaboration across teams...
A semantic HTML image component for displaying content images with captions, links, and visual effects.
<figure> and <figcaption> when caption present<picture> and <source>The image component renders optimized images with semantic HTML. The src can be passed as a positional argument (recommended) or keyword argument.
<%# Simple image (positional src argument - recommended) %>
<%= rui_image("photo.jpg", alt: "A beautiful sunset") %>
<%# Alternative: keyword argument for src %>
<%= rui_image(src: "photo.jpg", alt: "A beautiful sunset") %>
<%# Image with caption (uses figure/figcaption) %>
<%= rui_image(
"landscape.jpg",
alt: "Mountain vista",
caption: "View from the summit at sunrise"
) %>
<%# Linked image %>
<%= rui_image(
"product.jpg",
alt: "Product name",
url: product_path(@product)
) %>
Simple image with lazy loading and async decoding
When you provide a caption, the image automatically uses semantic <figure> and <figcaption> elements.
Without Caption
<img src="..." alt="...">
With Caption
<figure>
<img src="..." alt="...">
<figcaption>...</figcaption>
</figure>
<%= rui_image(
"photo.jpg",
alt: "Sunset over the ocean",
caption: "Malibu Beach - Summer 2024"
) %>
Control image dimensions with preset sizes.
xs (64px)
sm (96px)
md (128px)
lg (192px)
<%= rui_image(url, alt: "Photo", size: :xs) %> <%# 64px %>
<%= rui_image(url, alt: "Photo", size: :sm) %> <%# 96px %>
<%= rui_image(url, alt: "Photo", size: :md) %> <%# 128px %>
<%= rui_image(url, alt: "Photo", size: :lg) %> <%# 192px %>
<%= rui_image(url, alt: "Photo", size: :xl) %> <%# 256px %>
<%= rui_image(url, alt: "Photo", size: :full) %> <%# 100% width %>
Built-in presets for common social media image dimensions (based on 2025 guidelines). These presets include the correct aspect ratio automatically.
When using platform sizes, the aspect ratio is automatically applied. The ratio parameter is ignored for platform sizes.
:ig_post (1:1)
:ig_post_portrait (4:5)
:ig_story (9:16)
:ig_post_landscape (1.91:1)
:fb_post (1:1)
:fb_story (9:16)
:fb_cover (2.7:1)
:x_post (16:9)
:x_post_square (1:1)
:x_header (3:1)
:linkedin_post (1.91:1)
:linkedin_post_square (1:1)
:linkedin_cover (5.9:1)
:pin (2:3)
:tiktok_post (9:16)
:yt_thumbnail (16:9)
:pin_square (1:1)
<%# Instagram %>
<%= rui_image("campaign.jpg", alt: "Summer campaign", size: :ig_post) %>
<%= rui_image("story.jpg", alt: "New product", size: :ig_story) %>
<%= rui_image("reel.jpg", alt: "Tutorial", size: :ig_reel) %>
<%# Facebook %>
<%= rui_image("post.jpg", alt: "Announcement", size: :fb_post) %>
<%= rui_image("cover.jpg", alt: "Company cover", size: :fb_cover) %>
<%# X (Twitter) %>
<%= rui_image("news.jpg", alt: "Breaking news", size: :x_post) %>
<%= rui_image("banner.jpg", alt: "Profile banner", size: :x_header) %>
<%# LinkedIn %>
<%= rui_image("article.jpg", alt: "Industry insights", size: :linkedin_post) %>
<%# Pinterest %>
<%= rui_image("recipe.jpg", alt: "Recipe idea", size: :pin) %>
<%# TikTok %>
<%= rui_image("video.jpg", alt: "Dance tutorial", size: :tiktok_post) %>
<%# YouTube %>
<%= rui_image("thumb.jpg", alt: "Video title", size: :yt_thumbnail) %>
Four shape options for different visual styles.
Default
shape: :default
Rounded
shape: :rounded
Circle
shape: :circle
Square
shape: :square
Force images to specific aspect ratios for consistent layouts.
ratio: :square (1:1)
ratio: :landscape (4:3)
ratio: :video (16:9)
<%= rui_image(url, alt: "Photo", ratio: :square) %> <%# 1:1 %>
<%= rui_image(url, alt: "Photo", ratio: :video) %> <%# 16:9 %>
<%= rui_image(url, alt: "Photo", ratio: :portrait) %> <%# 3:4 %>
<%= rui_image(url, alt: "Photo", ratio: :landscape) %> <%# 4:3 %>
<%= rui_image(url, alt: "Photo", ratio: :wide) %> <%# 16:9 %>
<%= rui_image(url, alt: "Photo", ratio: :ultrawide) %> <%# 21:9 %>
Control how images fill their container.
fit: :cover
fit: :contain
fit: :fill
<%= rui_image(url, alt: "Photo", fit: :cover) %> <%# Crop to fill (default) %>
<%= rui_image(url, alt: "Photo", fit: :contain) %> <%# Letterbox %>
<%= rui_image(url, alt: "Photo", fit: :fill) %> <%# Stretch %>
<%= rui_image(url, alt: "Photo", fit: :none) %> <%# Natural size %>
<%= rui_image(url, alt: "Photo", fit: :scale_down) %> <%# Shrink only %>
Apply hover-activated visual effects to images. Hover over the images below to see the effects.
grayscale: true
Hover to see color
blur: true
Hover to unblur
sepia: true
Hover for color
zoom: true
Hover to zoom
shine: true
Hover for shine
overlay: true
Hover for overlay
<%# Combine multiple effects %>
<%= rui_image(
"photo.jpg",
alt: "Team member",
grayscale: true,
zoom: true,
shape: :rounded
) %>
<%# Professional team photo with grayscale + zoom %>
<%= rui_image(
@member.photo_url,
alt: @member.name,
grayscale: true,
zoom: true,
shape: :circle
) %>
Make images clickable by providing a URL.
<%= rui_image(
@product.image_url,
alt: @product.name,
url: product_path(@product),
shape: :rounded,
zoom: true
) %>
<%# With caption %>
<%= rui_image(
@product.image_url,
alt: @product.name,
caption: "#{@product.name} - #{number_to_currency(@product.price)}",
url: product_path(@product)
) %>
Built-in performance optimization with lazy loading and async decoding.
loading="lazy" - Defers loading until near viewportdecoding="async" - Non-blocking image decodefetchpriority="auto" - Browser decides priorityFor images visible immediately on page load, disable lazy loading:
<%# Hero image - load immediately %>
<%= rui_image(
"hero-banner.jpg",
alt: "Welcome to our store",
size: :full,
loading: :eager,
fetchpriority: :high,
decoding: :sync
) %>
Always provide width and height to prevent content jumping:
<%= rui_image("photo.jpg", alt: "Photo", width: 800, height: 600) %>
Serve different image sizes for different screen widths.
<%= rui_image(
"photo.jpg",
alt: "Responsive photo",
srcset: "photo-320.jpg 320w, photo-640.jpg 640w, photo-1280.jpg 1280w",
sizes: "(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw",
width: 1280,
height: 960
) %>
For images from CDNs or other domains:
<%= rui_image(
"https://cdn.example.com/image.jpg",
alt: "CDN image",
crossorigin: "anonymous",
referrerpolicy: "no-referrer"
) %>
The <picture> element provides advanced image handling for art direction and format fallback. Unlike srcset on a regular image (which serves different sizes of the same image), <picture> allows serving completely different images for different scenarios.
Serve modern, smaller formats to browsers that support them, with automatic fallback. The images look identical but have different file sizes.
Format fallback requires actual different format files on your server. The browser picks the first supported format. Images appear identical - only the file size/quality differs. Check DevTools Network tab to see which format loaded.
<%# AVIF → WebP → JPEG (images look the same, different sizes) %>
<%= rui_image(
"photo.jpg",
alt: "High quality photo",
sources: [
{ srcset: "photo.avif", type: "image/avif" },
{ srcset: "photo.webp", type: "image/webp" }
]
) %>
Show completely different images based on screen size. Resize your browser window to see the image change:
≥1024px: Foggy valley (wide landscape)
640-1023px: Forest path (medium)
<640px: Mountain peaks (portrait - fallback)
Drag your browser window smaller/larger to see completely different images load. This is art direction - not just resizing the same image.
<%# Different images per viewport %>
<%= rui_image(
"mobile-portrait.jpg",
alt: "Product showcase",
sources: [
{ srcset: "desktop-wide.jpg", media: "(min-width: 1024px)" },
{ srcset: "tablet-medium.jpg", media: "(min-width: 640px)" }
]
) %>
There are two approaches for dark mode images, depending on how your site handles theming:
If your site uses a JavaScript theme toggle (like data-theme="dark"), use Tailwind's dark: classes to show/hide images:
Light theme: Sunny beach | Dark theme: Starry night sky
Toggle the site's dark mode (top right) to see the image change. This works with JavaScript theme toggles.
<%# Works with JavaScript theme toggles %>
<div class="relative">
<%# Light mode image %>
<div class="block dark:hidden">
<%= rui_image("logo-light.png", alt: "Logo") %>
</div>
<%# Dark mode image %>
<div class="hidden dark:block">
<%= rui_image("logo-dark.png", alt: "Logo") %>
</div>
</div>
The prefers-color-scheme media query only responds to the operating system's dark mode setting, not JavaScript theme toggles:
prefers-color-scheme: dark only detects your OS dark mode setting (System Preferences → Appearance). It does NOT respond to website theme toggles that use data-theme or class-based switching.
<%# Only works with operating system dark mode setting %>
<%= rui_image(
"logo-light.png",
alt: "Company logo",
sources: [
{ srcset: "logo-dark.png", media: "(prefers-color-scheme: dark)" }
]
) %>
Combine picture element with semantic figure/figcaption. Resize your browser to see different images with the same caption:
≥768px: Wide banner crop | <768px: Square portrait
<%= rui_image(
"team-member-square.jpg",
alt: "Team member portrait",
caption: "Sarah Chen - CEO & Co-founder",
sources: [
{ srcset: "team-member-wide.jpg", media: "(min-width: 768px)" }
],
shape: :rounded,
alignment: :center
) %>
| Key | Required | Description |
|---|---|---|
| srcset | Yes | Image source(s) for this source element |
| media | No | Media query (e.g., "(min-width: 1024px)", "(prefers-color-scheme: dark)") |
| type | No | MIME type (e.g., "image/avif", "image/webp") |
| sizes | No | Responsive sizes for this source |
| width / height | No | Dimension hints for the browser |
Combine art direction with responsive sizing. Each source can have its own srcset with multiple resolutions:
<%# Different images per viewport, each with multiple sizes %>
<%= rui_image(
"hero-mobile.jpg",
alt: "Hero banner",
sources: [
{
srcset: "hero-desktop-800.jpg 800w, hero-desktop-1200.jpg 1200w, hero-desktop-1600.jpg 1600w",
media: "(min-width: 1024px)",
sizes: "(min-width: 1400px) 1400px, 100vw"
},
{
srcset: "hero-tablet-600.jpg 600w, hero-tablet-900.jpg 900w",
media: "(min-width: 640px)",
sizes: "100vw"
}
],
width: 1600,
height: 900
) %>
Serve higher resolution images for retina displays using density descriptors (1x, 2x, 3x):
<%# Serve 2x and 3x images for high DPI displays %>
<%= rui_image(
"logo.png",
alt: "Company logo",
sources: [
{ srcset: "logo.png 1x, logo@2x.png 2x, logo@3x.png 3x" }
]
) %>
<%# Combined with art direction %>
<%= rui_image(
"product-mobile.jpg",
alt: "Product image",
sources: [
{
srcset: "product-desktop.jpg 1x, product-desktop@2x.jpg 2x",
media: "(min-width: 768px)"
},
{
srcset: "product-mobile.jpg 1x, product-mobile@2x.jpg 2x"
}
]
) %>
The most advanced use case - different images per viewport, each with modern format fallbacks:
<%# Different images for desktop/mobile, with AVIF/WebP fallback %>
<%= rui_image(
"hero-mobile.jpg",
alt: "Product showcase",
sources: [
# Desktop: wide image with format fallback
{ srcset: "hero-desktop.avif", media: "(min-width: 1024px)", type: "image/avif" },
{ srcset: "hero-desktop.webp", media: "(min-width: 1024px)", type: "image/webp" },
{ srcset: "hero-desktop.jpg", media: "(min-width: 1024px)" },
# Tablet: medium image with format fallback
{ srcset: "hero-tablet.avif", media: "(min-width: 640px)", type: "image/avif" },
{ srcset: "hero-tablet.webp", media: "(min-width: 640px)", type: "image/webp" },
{ srcset: "hero-tablet.jpg", media: "(min-width: 640px)" },
# Mobile: format fallback only (no media query)
{ srcset: "hero-mobile.avif", type: "image/avif" },
{ srcset: "hero-mobile.webp", type: "image/webp" }
]
) %>
<picture>
<source srcset="photo.avif" type="image/avif">
<source srcset="photo.webp" type="image/webp">
<img src="photo.jpg" alt="Description" loading="lazy" decoding="async">
</picture>
<div class="grid grid-cols-4 gap-4">
<% @products.each do |product| %>
<%= rui_image(
product.image_url,
alt: product.name,
ratio: :square,
shape: :rounded,
zoom: true,
url: product_path(product)
) %>
<% end %>
</div>
CEO
CTO
Designer
Engineer
<div class="grid grid-cols-4 gap-6">
<% @team_members.each do |member| %>
<div class="text-center">
<%= rui_image(
member.photo_url,
alt: member.name,
caption: member.name,
size: :lg,
shape: :circle,
grayscale: true,
alignment: :center
) %>
<%= rui_text(member.role, size: :sm, color: :muted, class: "mt-1") %>
</div>
<% end %>
</div>
How We Built Our Design System
Building a comprehensive design system requires careful planning and collaboration across teams...
The Image component follows WAI-ARIA best practices for accessible images.
alt attribute required for meaningful images (describes content)
alt="" for decorative images (hidden from screen readers)
role="img" implicit on <img> elements
Tab
Enter activates linked images
<img> element for standard images
<figure> + <figcaption> for images with captions
<picture> element for responsive art direction
loading="lazy" and decoding="async" for performance
<figcaption>
<%# Meaningful image with descriptive alt text %>
<%= rui_image("team-photo.jpg", alt: "Marketing team celebrating product launch") %>
<%# Decorative image (hidden from screen readers) %>
<%= rui_image("decorative-border.png", alt: "") %>
<%# Image with caption (uses figure/figcaption) %>
<%= rui_image(
"chart.png",
alt: "Sales growth chart showing 45% increase",
caption: "Q4 2024 sales performance by region"
) %>
<%# Linked image (alt text describes destination) %>
<%= rui_image(
"product.jpg",
alt: "View Blue Widget product details",
url: product_path(@product)
) %>
Semantic HTML image with captions, links, visual effects, and picture element support
| Parameter | Type | Default | Description |
|---|---|---|---|
| src* | String | — | Image source URL (positional or keyword) |
| alt* | String | — | Alt text for accessibility |
| caption | String | — | Caption text (triggers figure/figcaption) |
| url | String | — | Link URL (makes image clickable) |
Size and dimension options
| Parameter | Type | Default | Description |
|---|---|---|---|
| size | Symbol |
:auto
|
Size preset
:auto
:full
:xs
:sm
:base
:lg
:xl
:xl2
:xl3
|
| width | Integer/String | — | Width in pixels or CSS value |
| height | Integer/String | — | Height in pixels or CSS value |
| aspect | Symbol | — |
Aspect ratio
:square
:video
:wide
:portrait
:auto
|
Visual styling options
| Parameter | Type | Default | Description |
|---|---|---|---|
| shape | Symbol |
:square
|
Corner style
:square
:rounded
:pill
:circle
|
| fit | Symbol |
:cover
|
Object fit behavior
:cover
:contain
:fill
:none
:scale_down
|
| filter | Symbol | — |
CSS filter effect
:grayscale
:sepia
:blur
:brightness
:contrast
|
| hover_effect | Symbol | — |
Hover effect
:zoom
:brighten
:darken
:grayscale_to_color
|
Loading and performance options
| Parameter | Type | Default | Description |
|---|---|---|---|
| loading | Symbol |
:lazy
|
Loading behavior
:lazy
:eager
|
| srcset | String | — | Responsive source set |
| sizes | String | — | Responsive sizes attribute |
| sources | Array | — | Picture element sources for art direction |
Social media preset sizes
| Parameter | Type | Default | Description |
|---|---|---|---|
| :ig_post | Preset | — | Instagram post (1080x1080) |
| :ig_story | Preset | — | Instagram story (1080x1920) |
| :fb_cover | Preset | — | Facebook cover (820x312) |
| :twitter_header | Preset | — | Twitter header (1500x500) |
| :og_image | Preset | — | Open Graph (1200x630) |