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
Variants
Details
Was Platzhalterbilder mitbringt, aus Registry und Quellcode gelesen.
- Kategorie
- Platzhalter und Generatoren
- Bereich
- Visuals
- Variants
- 5
- Version
- 1.0.0 · seit 1.0.0
- Datei
- components/ui/placeholder.tsx
- npm-Pakete
- Keine
- Sprachen
- Englisch und Deutsch, 18 eigene Texte
- Studio
- Im Code anpassen
- Mit KI bauen
- Per MCP in Claude, Cursor, Copilot und Codex
Props
Die Schnittstelle PlaceholderProps aus components/ui/placeholder.tsx. Beschreibungen stammen aus den Kommentaren im Code und sind auf Englisch.
industryoptionalPlaceholderIndustryrestaurantcafebarreal-estatehotelfitnessyogasalonpracticelawcrafttravelsoftwareeducationeventsshopagency
seedoptionalnumberPicks another shot of the same industry
aspectoptionalstringCSS aspect ratio, default "4 / 3". Pass "auto" to size it with className.
roundedoptionalbooleanRounded corners and a hairline edge, default true
labeloptionalReact.ReactNodeAn optional caption chip in the lower left
showRatiooptionalbooleanA small size hint for builders in the upper right, e.g. 1200 × 900
ratioWidthoptionalnumberWidth the size hint is based on, default 1200
zoomoptionalbooleanZooms the picture a little on hover, default false
cropoptionalnumberCrops into the picture for a detail shot, 1 shows all of it (default), 2 shows a quarter
focusoptionalstringThe point the crop centers on, as CSS position, default "50% 50%"
altoptionalstringAlternative 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
| Schlüssel | Englisch | Deutsch |
|---|---|---|
| restaurant | Englisch A dish on a restaurant table | Deutsch Ein Gericht auf einem Restauranttisch |
| cafe | Englisch Coffee in a café in morning light | Deutsch Kaffee in einem Café im Morgenlicht |
| bar | Englisch A drink on the bar | Deutsch Ein Drink an der Bar |
| real-estate | Englisch A house for sale | Deutsch Ein Haus zum Verkauf |
| hotel | Englisch A hotel room with a view | Deutsch Ein Hotelzimmer mit Aussicht |
| fitness | Englisch A training studio | Deutsch Ein Trainingsstudio |
| yoga | Englisch A calm yoga studio | Deutsch Ein ruhiges Yogastudio |
| salon | Englisch A salon with a mirror and a chair | Deutsch Ein Salon mit Spiegel und Stuhl |
| practice | Englisch A bright treatment room | Deutsch Ein heller Behandlungsraum |
| law | Englisch An office with books and a desk | Deutsch Ein Büro mit Büchern und Schreibtisch |
| craft | Englisch A workshop bench with ceramics | Deutsch Eine Werkbank mit Keramik |
| travel | Englisch A landscape for a trip | Deutsch Eine Landschaft für eine Reise |
| software | Englisch A software dashboard | Deutsch Ein Software-Dashboard |
| education | Englisch A course cover | Deutsch Ein Kurscover |
| events | Englisch A conference stage | Deutsch Eine Konferenzbühne |
| shop | Englisch A small boutique | Deutsch Eine kleine Boutique |
| agency | Englisch A brand identity board | Deutsch Ein Markenboard |
| ratio | Englisch Image size | Deutsch 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
'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.