immich-app on GitHub

immich-app/immich

Reviewed against Rams quality heuristics — accessibility, hierarchy, color, typography, spacing, components, motion, and UX.

30 files reviewed·May 13, 2026

Top fix

Remove debug border-red-400 from email card before next deploy.

See the fix

Verdict

Debug artifacts are shipping straight to production — red borders on a privacy page and live email templates signal a broken review process, not just careless commits. The email red-border issue is the most urgent: real users are already receiving visually broken messages.

Files Rams reviewed

web/src/lib/components/pages/SharedLinkPage.svelte

server/src/emails/components/futo.layout.tsx

server/src/emails/components/immich.layout.tsx

web/src/lib/components/pages/SharedLinkErrorPage.svelte

docs/src/pages/privacy-policy.tsx

docs/src/components/timeline.tsx

docs/src/components/version-switcher.tsx

docs/src/pages/index.tsx

server/src/emails/components/footer.template.tsx

web/src/lib/components/AdaptiveImage.svelte

web/src/lib/components/ApiKeyPermissionsPicker.svelte

web/src/lib/components/BreadcrumbActionPage.svelte

web/src/lib/components/Image.svelte

web/src/lib/components/ImageLayer.svelte

web/src/lib/components/SharedLinkExpiration.svelte

web/src/lib/components/SharedLinkFormFields.svelte

web/src/lib/components/Thumbhash.svelte

web/src/lib/components/admin-settings/StorageTemplateSettings.svelte

web/src/lib/components/admin-settings/SupportedDatetimePanel.svelte

web/src/lib/components/admin-settings/SupportedVariablesPanel.svelte

web/src/lib/components/album-page/AlbumCard.svelte

web/src/lib/components/album-page/AlbumCardGroup.svelte

web/src/lib/components/album-page/AlbumMap.svelte

web/src/lib/components/album-page/AlbumSharedLink.svelte

web/src/lib/components/album-page/AlbumViewer.svelte

web/src/lib/components/album-page/AlbumsList.svelte

web/src/lib/components/album-page/AlbumsTable.svelte

web/src/lib/components/album-page/AlbumsTableHeader.svelte

web/src/lib/components/album-page/AlbumsTableRow.svelte

web/src/lib/components/asset-viewer/ActivityStatus.svelte

89/100

UX

3 critical1 serious
UX·docs/src/pages/privacy-policy.tsx:6Critical

Debug border-red-400 left on the outer section in production

The outer `<section>` carries `border border-red-400` — a bright red 1px border wrapping the entire policy document. This reads as a debug artifact, not a design choice.

Why it matters

A red border around the privacy policy signals an unfinished or broken page to every visitor. It undermines the credibility of a page that exists specifically to communicate trustworthiness.

Fix

Remove debug border values and use a neutral border token or no border at all for content containers.

<section className="max-w-[900px] m-4 p-4 md:p-6 md:m-auto md:my-12 border border-red-400 rounded-2xl  bg-slate-200 dark:bg-immich-dark-gray">
<section className="max-w-[900px] m-4 p-4 md:p-6 md:m-auto md:my-12 rounded-2xl bg-slate-200 dark:bg-immich-dark-gray">
UX·server/src/emails/components/futo.layout.tsx:60Critical

Debug red border on main card will ship to real email recipients

The main card `<Section>` has `border border-red-400` applied. This is a visible red border around every email this layout wraps — it reads as a broken or flagged message to any recipient.

Why it matters

Every email sent using this layout — account confirmations, password resets, notifications — will render with a red border. Recipients will read it as an error state or a spam signal.

Fix

Remove debug border classes before merging; use `border-futo-gray` or no border if a subtle container edge is needed.

<Section className="my-6 p-12 border border-red-400 rounded-[50px] bg-gray-50">
<Section className="my-6 p-12 border border-futo-gray rounded-[50px] bg-gray-50">
UX·server/src/emails/components/immich.layout.tsx:54Critical

Debug border-red-400 will render as red outline in production emails

The main card `<Section>` has `border border-red-400 rounded-[50px]`. A bright red border on the email container looks like a broken or alarming message to recipients — not a visual style.

Why it matters

Every email sent with this layout will display a red border around the content card. This reads as a broken or flagged message to recipients and is almost certainly a leftover debugging class.

Fix

Remove debug border classes; use a neutral or no border for the email card container.

<Section className="my-6 p-12 border border-red-400 rounded-[50px] bg-gray-50">
<Section className="my-6 p-12 border border-gray-200 rounded-[50px] bg-gray-50">
UX·web/src/lib/components/pages/SharedLinkErrorPage.svelte:13Serious

Error page offers no recovery path — users are stranded

The page displays the error heading and an optional error message, then ends. There is no link back to the home page, no "Go back" action, and no contextual suggestion for what the user can do next.

Why it matters

Without a recovery path, users must rely on browser back/forward controls or manually edit the URL — onboarding friction compounds because the dead end feels like an app failure rather than a navigation error.

Fix

Every error state must offer at least one explicit recovery action adjacent to the error message.

{#if page.error?.message}
  <h2 class="text-xl text-immich-fg dark:text-immich-dark-fg">{page.error.message}</h2>
{/if}
{#if page.error?.message}
  <h2 class="text-xl text-immich-fg dark:text-immich-dark-fg">{page.error.message}</h2>
{/if}
<a href="/" class="mt-6 text-sm text-primary underline hover:opacity-80">{$t('go_to_homepage')}</a>
95/100

Accessibility

1 critical1 serious
Accessibility·web/src/lib/components/pages/SharedLinkPage.svelte:86Critical

Password input has no label — screen readers announce nothing

The `<PasswordInput>` component receives only a `placeholder="Password"` prop and no associated label or `aria-label`. Placeholder text disappears on input and is never announced as a field label by assistive tech.

Why it matters

Screen reader users land on an unlabeled input with no way to know what it expects. Placeholder-only patterns also fail WCAG 1.3.1 — the field purpose is not programmatically determinable.

Fix

Every form input must have an associated visible label or aria-label that persists regardless of input state.

<PasswordInput autocomplete="off" bind:value={password} placeholder="Password" />
<PasswordInput autocomplete="off" bind:value={password} placeholder={$t('password')} aria-label={$t('password')} />
Accessibility·docs/src/components/timeline.tsx:56Serious

Immich logo image has no alt text, invisible to screen readers

The `<img src="/img/immich-logo.svg" />` rendered when `cardIcon === 'immich'` has no `alt` attribute. Every other icon in the list is rendered via `<Icon path={cardIcon} />` which at minimum announces nothing — but the img tag actively fails WCAG 1.1.1.

Why it matters

Screen readers announce the filename or nothing at all, breaking the item's heading context for assistive tech users scanning the timeline.

Fix

Every content image needs descriptive alt text; decorative images need alt="" plus aria-hidden.

<img src="/img/immich-logo.svg" height="30" className="rounded-none" />
<img src="/img/immich-logo.svg" height="30" alt="Immich" className="rounded-none" />

Hierarchy

✓ No issues found

Typography

✓ No issues found

Color

✓ No issues found

Spacing

✓ No issues found

Components

✓ No issues found

Motion

✓ No issues found

Working well

  • Defining the color palette and font family inside the `<Tailwind config>` block rather than scattering inline styles throughout child templates is the right architecture. All email tokens are colocated in one place, which makes the system auditable and easy to update. The mistake is just not using those tokens consistently in this same file.
  • Loading Overpass via `<Font>` with a `woff2` source and a `'100 900'` weight range is the right call for email — it ensures the variable font loads efficiently in clients that support it while the `fallbackFontFamily` handles everything else. The `fontWeight` range prevents multiple font-face declarations for individual weights.
  • Using `await tick()` before `navigate()` after a successful password login is deliberate and correct: it ensures Svelte has flushed reactive state (the newly set `sharedLink`, `passwordRequired = false`) before the router reads URL params. Most implementations skip this and hit race conditions.
  • Using `<strong>` for the data-type labels like "Locally Stored Data" and "Purchase Information" inside body paragraphs is the right call — it creates visual anchors for scanning without inflating the heading hierarchy, keeping the document outline clean while still aiding skim reading.

Score your own repo with Rams.

Free on public repos. Reviews run on every pull request and post inline fix suggestions.

Review my public repoFree