Zum Inhalt springen
UI Kit
Accent

Platzhalterbilder

Platzhalterbilder, die wie das Foto aussehen, das später in den Platz kommt. Siebzehn Branchen mit je mehreren Motiven, einem Bildchip und einem Größenhinweis für Bauende. Keine Dateien, keine Anfragen.

Eine von 3 Components in Platzhalter und Generatoren

5 Variants

Variants

Gastronomie

Restaurant, Café, Bar und Hotel. Die Karten steigen nacheinander auf, das Bild zoomt beim Überfahren und der Knopf tauscht jedes Motiv.

Immobilienanzeige

Eine Anzeige mit Haus und Räumen. Das Foto wächst beim Scrollen an seinen Platz, die Vorschaubilder wechseln es und der Hinweis zeigt 1600 px.

Gesundheit und Wellness

Fitness, Yoga, Salon und Praxis. Der Umschalter gleitet zur Branche und die drei Motive blenden neu ein.

Business und Studio

Kanzlei, Software, Agentur, Kurs, Event, Laden und Handwerk in einem Bento-Raster, das der Reihe nach aufsteigt.

Alle Branchen

Das ganze Set mit Gruppenfilter. Der Schalter zeigt die Pixelgröße, die jeder Platz erwartet.

Details

Was Platzhalterbilder mitbringt, aus Registry und Quellcode gelesen.

Bereich
Visuals
Variants
5
Version
1.0.0 · seit 1.0.0
Datei
components/ui/placeholder.tsx
Bringt mit
utilskitgraphicsphotos
npm-Pakete
Keine
Sprachen
Englisch und Deutsch, 18 eigene Texte
Studio
Im Code anpassen

Props

Die Schnittstelle PlaceholderProps aus components/ui/placeholder.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.

  • industryoptional
    PlaceholderIndustry

    restaurantcafebarreal-estatehotelfitnessyogasalonpracticelawcrafttravelsoftwareeducationeventsshopagency

  • seedoptional
    number

    Picks another shot of the same industry

  • aspectoptional
    string

    CSS aspect ratio, default "4 / 3". Pass "auto" to size it with className.

  • roundedoptional
    boolean

    Rounded corners and a hairline edge, default true

  • labeloptional
    React.ReactNode

    An optional caption chip in the lower left

  • showRatiooptional
    boolean

    A small size hint for builders in the upper right, e.g. 1200 × 900

  • ratioWidthoptional
    number

    Width the size hint is based on, default 1200

  • zoomoptional
    boolean

    Zooms the picture a little on hover, default false

  • cropoptional
    number

    Crops into the picture for a detail shot, 1 shows all of it (default), 2 shows a quarter

  • focusoptional
    string

    The point the crop centers on, as CSS position, default "50% 50%"

  • altoptional
    string

    Alternative text. Defaults to a short description of the industry, pass "" for a decorative image.

Eingebaute Texte

Diese Texte schreibt Platzhalterbilder selbst. Die Sprache folgt dem KitProvider, über die Prop labels ersetzt du einzelne.

Alle 18 Texte anzeigen
restaurantEnglisch A dish on a restaurant tableDeutsch Ein Gericht auf einem Restauranttisch
cafeEnglisch Coffee in a café in morning lightDeutsch Kaffee in einem Café im Morgenlicht
barEnglisch A drink on the barDeutsch Ein Drink an der Bar
real-estateEnglisch A house for saleDeutsch Ein Haus zum Verkauf
hotelEnglisch A hotel room with a viewDeutsch Ein Hotelzimmer mit Aussicht
fitnessEnglisch A training studioDeutsch Ein Trainingsstudio
yogaEnglisch A calm yoga studioDeutsch Ein ruhiges Yogastudio
salonEnglisch A salon with a mirror and a chairDeutsch Ein Salon mit Spiegel und Stuhl
practiceEnglisch A bright treatment roomDeutsch Ein heller Behandlungsraum
lawEnglisch An office with books and a deskDeutsch Ein Büro mit Büchern und Schreibtisch
craftEnglisch A workshop bench with ceramicsDeutsch Eine Werkbank mit Keramik
travelEnglisch A landscape for a tripDeutsch Eine Landschaft für eine Reise
softwareEnglisch A software dashboardDeutsch Ein Software-Dashboard
educationEnglisch A course coverDeutsch Ein Kurscover
eventsEnglisch A conference stageDeutsch Eine Konferenzbühne
shopEnglisch A small boutiqueDeutsch Eine kleine Boutique
agencyEnglisch A brand identity boardDeutsch Ein Markenboard
ratioEnglisch Image sizeDeutsch Bildgröße

Barrierefreiheit

Aus dem Quellcode der Component abgeleitet.

  • Setzt aria-label, damit Screenreader Zweck und Zustand ansagen.
  • Nutzt die Rolle text.
  • Der Server rendert den Startzustand vollständig, JavaScript ergänzt nur die Interaktion.

Bewegung

  • Bei reduzierter Bewegung (prefers-reduced-motion) bleibt sie ruhig. 1 Stellen im Code schalten Bewegung dann ab.
  • Ohne Animationen, sie steht sofort ruhig da.

Quellcode

Die Datei, die aiw add in dein Projekt legt. Sie gehört danach dir.

Datei anzeigen, 257 Zeilen
components/ui/placeholder.tsx
'use client';

import { useEffect, useRef, useState } from 'react';
import { cn, useTexts } from '@/lib/kit';
import { AppScreen, TourScreen, type TourView } from '@/components/ui/graphics';
import {
  BrandBoard,
  CourseCover,
  PhotoCafe,
  PhotoCoast,
  PhotoDish,
  PhotoDrink,
  PhotoHouse,
  PhotoInterior,
  PhotoOffice,
  PhotoShop,
  PhotoStudio,
  PhotoTreatment,
  PhotoValley,
  PhotoWorkshop,
  StagePhoto,
  type DishKind,
  type DrinkKind,
  type HouseKind,
  type InteriorKind,
  type InteriorView,
  type StudioKind,
} from '@/components/ui/photos';

/*
 * Placeholder images per industry. They look like the photo that will later
 * sit in the slot, so a layout reads right before the real pictures arrive.
 * Drawn in SVG, no image files, no requests. The seed picks a different shot
 * of the same industry. Photo motifs keep their natural colours in both
 * themes, software and agency follow the tokens and the accent.
 */

export type PlaceholderIndustry =
  | 'restaurant'
  | 'cafe'
  | 'bar'
  | 'real-estate'
  | 'hotel'
  | 'fitness'
  | 'yoga'
  | 'salon'
  | 'practice'
  | 'law'
  | 'craft'
  | 'travel'
  | 'software'
  | 'education'
  | 'events'
  | 'shop'
  | 'agency';

export const PLACEHOLDER_INDUSTRIES: PlaceholderIndustry[] = ['restaurant', 'cafe', 'bar', 'real-estate', 'hotel', 'fitness', 'yoga', 'salon', 'practice', 'law', 'craft', 'travel', 'software', 'education', 'events', 'shop', 'agency'];

const TEXTS = {
  en: {
    restaurant: 'A dish on a restaurant table',
    cafe: 'Coffee in a café in morning light',
    bar: 'A drink on the bar',
    'real-estate': 'A house for sale',
    hotel: 'A hotel room with a view',
    fitness: 'A training studio',
    yoga: 'A calm yoga studio',
    salon: 'A salon with a mirror and a chair',
    practice: 'A bright treatment room',
    law: 'An office with books and a desk',
    craft: 'A workshop bench with ceramics',
    travel: 'A landscape for a trip',
    software: 'A software dashboard',
    education: 'A course cover',
    events: 'A conference stage',
    shop: 'A small boutique',
    agency: 'A brand identity board',
    ratio: 'Image size',
  },
  de: {
    restaurant: 'Ein Gericht auf einem Restauranttisch',
    cafe: 'Kaffee in einem Café im Morgenlicht',
    bar: 'Ein Drink an der Bar',
    'real-estate': 'Ein Haus zum Verkauf',
    hotel: 'Ein Hotelzimmer mit Aussicht',
    fitness: 'Ein Trainingsstudio',
    yoga: 'Ein ruhiges Yogastudio',
    salon: 'Ein Salon mit Spiegel und Stuhl',
    practice: 'Ein heller Behandlungsraum',
    law: 'Ein Büro mit Büchern und Schreibtisch',
    craft: 'Eine Werkbank mit Keramik',
    travel: 'Eine Landschaft für eine Reise',
    software: 'Ein Software-Dashboard',
    education: 'Ein Kurscover',
    events: 'Eine Konferenzbühne',
    shop: 'Eine kleine Boutique',
    agency: 'Ein Markenboard',
    ratio: 'Bildgröße',
  },
};

export type PlaceholderLabels = Partial<typeof TEXTS.en>;

export interface PlaceholderProps {
  industry?: PlaceholderIndustry;
  /** Picks another shot of the same industry */
  seed?: number;
  /** CSS aspect ratio, default "4 / 3". Pass "auto" to size it with className. */
  aspect?: string;
  /** Rounded corners and a hairline edge, default true */
  rounded?: boolean;
  /** An optional caption chip in the lower left */
  label?: React.ReactNode;
  /** A small size hint for builders in the upper right, e.g. 1200 × 900 */
  showRatio?: boolean;
  /** Width the size hint is based on, default 1200 */
  ratioWidth?: number;
  /** Zooms the picture a little on hover, default false */
  zoom?: boolean;
  /** Crops into the picture for a detail shot, 1 shows all of it (default), 2 shows a quarter */
  crop?: number;
  /** The point the crop centers on, as CSS position, default "50% 50%" */
  focus?: string;
  /** Alternative text. Defaults to a short description of the industry, pass "" for a decorative image. */
  alt?: string;
  labels?: PlaceholderLabels;
  className?: string;
}

const DISHES: DishKind[] = ['pasta', 'salad', 'fish', 'steak', 'risotto', 'bowl', 'soup', 'dessert'];
const DRINKS: DrinkKind[] = ['cocktail', 'wine', 'spritz', 'beer', 'red', 'lemonade'];
const HOUSES: HouseKind[] = ['villa', 'townhouse', 'cottage', 'apartment', 'loft'];
const ROOMS: [InteriorKind, InteriorView][] = [['suite', 'sea'], ['bedroom', 'mountains'], ['living', 'city'], ['bath', 'garden']];
const STUDIOS: StudioKind[] = ['strength', 'cycle', 'boxing'];
const CALM: StudioKind[] = ['yoga', 'pilates'];
const VIEWS: TourView[] = ['insights', 'board', 'automations', 'inbox'];

/** A product shot for software: the app window floats on a soft accent field */
function SoftwareShot({ seed }: { seed: number }) {
  return (
    <div className="absolute inset-0 bg-[radial-gradient(120%_90%_at_20%_0%,color-mix(in_oklab,var(--primary)_30%,var(--surface-2)),var(--surface)_70%)]">
      <div aria-hidden className="absolute inset-0 bg-[radial-gradient(color-mix(in_oklab,var(--foreground)_10%,transparent)_1px,transparent_1.4px)] [background-size:18px_18px] [mask-image:linear-gradient(to_bottom,#000,transparent_80%)]" />
      <svg viewBox="0 0 800 560" preserveAspectRatio="xMidYMin slice" className="absolute inset-0 size-full">
        <foreignObject x="90" y="56" width="620" height="560">
          <div className="h-full overflow-hidden rounded-xl border border-border bg-background shadow-[var(--shadow-lg)]">
            <div className="flex h-7 items-center gap-1.5 border-b border-border bg-surface px-3">
              {[0, 1, 2].map((i) => (
                <span key={i} className="size-2 rounded-full bg-border-strong" />
              ))}
            </div>
            {seed % 2 === 0 ? <AppScreen className="min-h-[520px]" /> : <TourScreen view={VIEWS[Math.floor(seed / 2) % VIEWS.length]} className="min-h-[520px]" />}
          </div>
        </foreignObject>
      </svg>
    </div>
  );
}

function art(industry: PlaceholderIndustry, seed: number) {
  const s = Math.abs(Math.round(seed));
  switch (industry) {
    case 'restaurant':
      return <PhotoDish dish={DISHES[s % DISHES.length]} table={s} />;
    case 'cafe':
      return s % 2 === 0 ? <PhotoCafe /> : <PhotoDrink kind="coffee" />;
    case 'bar':
      return <PhotoDrink kind={DRINKS[s % DRINKS.length]} />;
    case 'real-estate':
      return <PhotoHouse kind={HOUSES[s % HOUSES.length]} />;
    case 'hotel': {
      const [kind, view] = ROOMS[s % ROOMS.length];
      return <PhotoInterior kind={kind} view={view} />;
    }
    case 'fitness':
      return <PhotoStudio kind={STUDIOS[s % STUDIOS.length]} />;
    case 'yoga':
      return <PhotoStudio kind={CALM[s % CALM.length]} />;
    case 'salon':
      return <PhotoTreatment kind={s % 2 === 0 ? 'salon' : 'spa'} />;
    case 'practice':
      return <PhotoTreatment kind="practice" />;
    case 'law':
      return <PhotoOffice seed={s} />;
    case 'craft':
      return <PhotoWorkshop />;
    case 'travel':
      return s % 2 === 0 ? <PhotoCoast /> : <PhotoValley />;
    case 'software':
      return <SoftwareShot seed={s} />;
    case 'education':
      return <CourseCover seed={s} />;
    case 'events':
      return <StagePhoto seed={s} anchor="xMidYMin" />;
    case 'shop':
      return <PhotoShop seed={s} />;
    case 'agency':
      return <BrandBoard seed={s} />;
  }
}

/** "4 / 3" → [4, 3] */
function ratioOf(aspect: string): [number, number] | null {
  const m = aspect.match(/^\s*([\d.]+)\s*(?:\/\s*([\d.]+))?\s*$/);
  if (!m) return null;
  return [Number(m[1]), Number(m[2] ?? 1)];
}

/**
 * A placeholder picture for an industry.
 *
 *   <Placeholder industry="restaurant" seed={2} aspect="3 / 2" label="Lunch menu" showRatio />
 */
export function Placeholder({ industry = 'restaurant', seed = 0, aspect = '4 / 3', rounded = true, label, showRatio = false, ratioWidth = 1200, zoom = false, crop = 1, focus = '50% 50%', alt, labels, className }: PlaceholderProps) {
  const t = useTexts(TEXTS, labels);
  const text = alt ?? t[industry];
  // with aspect="auto" the size hint follows the rendered box
  const ref = useRef<HTMLDivElement>(null);
  const [box, setBox] = useState<[number, number] | null>(null);
  useEffect(() => {
    const el = ref.current;
    if (!el || !showRatio || aspect !== 'auto' || typeof ResizeObserver === 'undefined') return;
    const ro = new ResizeObserver(() => setBox(el.clientWidth && el.clientHeight ? [el.clientWidth, el.clientHeight] : null));
    ro.observe(el);
    return () => ro.disconnect();
  }, [showRatio, aspect]);
  const r = aspect === 'auto' ? box : ratioOf(aspect);
  const hint = r ? `${Math.round(ratioWidth)} × ${(aspect === 'auto' ? Math.round((ratioWidth * r[1]) / r[0] / 10) * 10 : Math.round((ratioWidth * r[1]) / r[0]))}` : null;
  return (
    <div
      ref={ref}
      role={text ? 'img' : undefined}
      aria-label={text || undefined}
      aria-hidden={text ? undefined : true}
      className={cn('group/ph relative isolate overflow-hidden bg-surface-2', rounded && 'rounded-xl border border-border', className)}
      style={aspect !== 'auto' ? { aspectRatio: aspect } : undefined}
    >
      <div aria-hidden data-clip-ok className={cn('absolute inset-0', zoom && 'transition-transform duration-700 ease-[var(--ease-kit)] group-hover/ph:scale-[1.045] motion-reduce:transition-none')}>
        <div className="absolute inset-0 [&>svg]:absolute [&>svg]:inset-0" style={crop !== 1 ? { transform: `scale(${Math.max(1, crop)})`, transformOrigin: focus } : undefined}>
          {art(industry, seed)}
        </div>
      </div>
      {showRatio && hint && (
        <span title={`${t.ratio} ${hint}`} className="absolute right-2.5 top-2.5 inline-flex items-center gap-1 rounded-md bg-[color-mix(in_oklab,var(--background)_78%,transparent)] px-1.5 py-1 text-[10.5px] font-medium leading-none tabular-nums text-foreground shadow-[var(--shadow-sm)] backdrop-blur">
          <svg viewBox="0 0 12 12" className="size-2.5" fill="none" stroke="currentColor" strokeWidth="1.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
            <path d="M1.5 4.5v-3h3M10.5 7.5v3h-3M1.5 1.5l3.5 3.5M10.5 10.5 7 7" />
          </svg>
          {hint}
        </span>
      )}
      {label && (
        <span className="absolute bottom-2.5 left-2.5 max-w-[calc(100%-1.25rem)] truncate rounded-md bg-[color-mix(in_oklab,var(--background)_82%,transparent)] px-2 py-1 text-[11.5px] font-medium leading-tight text-foreground shadow-[var(--shadow-sm)] backdrop-blur">
          {label}
        </span>
      )}
    </div>
  );
}

Häufige Fragen

Wie installiere ich Platzhalterbilder?

Mit aiw add @ai-website/placeholder. Der Befehl legt die Datei components/ui/placeholder.tsx in dein Projekt, der Code gehört danach dir. Mit dabei sind graphics und photos. Weitere npm-Pakete braucht sie nicht.

Welche Varianten hat Platzhalterbilder?

5 Varianten, Gastronomie, Immobilienanzeige, Gesundheit und Wellness, Business und Studio und Alle Branchen.

Kann ich die Texte von Platzhalterbilder übersetzen?

Ja. Platzhalterbilder bringt 18 eigene Texte auf Deutsch und Englisch mit, etwa „Ein Gericht auf einem Restauranttisch“, „Kaffee in einem Café im Morgenlicht“ und „Ein Drink an der Bar“. Die Sprache folgt dem KitProvider, einzelne Texte ersetzt du über die Prop labels.

Kann ein KI-Assistent Platzhalterbilder einbauen?

Ja. Über den MCP-Server der aiw CLI finden Claude, Cursor, GitHub Copilot, Windsurf, Codex und Gemini Platzhalterbilder mit aiw_components und installieren sie mit aiw_add @ai-website/placeholder.

Oft gesucht

Direkt zu

In die Zwischenablage kopiert