Awards section
Awards, prizes and seals with every mark drawn in the kit: laurels that draw themselves, round seals with text on the rim, a medal on a ribbon, awards by year and a compact strip for next to a button.
Recognition, drawn in your style
An awards section shows that others have already judged your work. Every mark here is drawn in the kit instead of using foreign badges, so laurels, seals and medals take your colors and fonts and never look like pasted images.
Laurels sets a row of wreaths with rank, award and year inside, the branches draw themselves and the leaves appear one by one when the row comes into view. Seals are round seals with a toothed edge, the issuer on the upper rim and the year below, the rim turns a little under the pointer. Trophy case shows the latest prize as a medal on a ribbon with a light passing over it, next to a rating with stars and the other awards by year. By year has a year picker that moves with the arrow keys, the year set huge behind and the awards of that year as cards. Compact strip puts a rating with stars and small seals into one card for under a hero or next to the buy button.
Show the awards that matter to your buyers, a regional prize can beat a famous one for a local business. Always give the year, recognition ages. Place the compact strip close to the decision, under the hero or next to the price.
Variants
Details
What Awards brings along, read from the registry and the source.
- Category
- Press and awards
- Area
- Marketing
- Variants
- 5
- Version
- 1.0.0 · since 1.2.0
- File
- components/ui/awards.tsx
- npm packages
- None
- Languages
- English and German, 9 texts of its own
- Studio
- Adjust in code
- Build with AI
- Over MCP in Claude, Cursor, Copilot and Codex
Props
The AwardsProps interface from components/ui/awards.tsx, with the comments from the code.
variantoptionalAwardsVariantlaurelssealstrophy-caseby-yearstrip
eyebrowoptionalReact.ReactNodetitleoptionalReact.ReactNodedescriptionoptionalReact.ReactNodeitemsrequiredAwardItem[]ratingoptional{ value: number; count: number; source?: string }strip, trophy-case: an average rating
ctaoptional{ label: string; href: string }
Built-in texts
Awards writes these texts itself. The language follows the KitProvider, the labels prop replaces single ones.
Show all 9 texts
| Key | English | German |
|---|---|---|
| count | English {n} awards | German {n} Auszeichnungen |
| count1 | English 1 award | German 1 Auszeichnung |
| year | English Year | German Jahr |
| all | English All years | German Alle Jahre |
| winner | English Winner | German Gewinner |
| rating | English {v} out of 5 | German {v} von 5 |
| from | English from {n} reviews | German aus {n} Bewertungen |
| more | English Read about it | German Mehr dazu |
| awards | English Awards | German Auszeichnungen |
Accessibility
Read from the source of the component.
- Awards handles the keys Arrow left, Arrow right and Space itself.
- Sets aria-controls, aria-label, aria-labelledby and aria-selected, so screen readers announce purpose and state.
- Uses the roles tab, tablist and tabpanel.
- Adds text for screen readers where icons and graphics stand alone.
- Shows a visible focus ring when used with the keyboard.
- The server renders the starting state in full, JavaScript only adds the interaction.
Motion
- Rises in briefly the first time it comes into view (kit-rise, useInView).
- Brings animations of its own, calmly timed and without wobble.
- With reduced motion (prefers-reduced-motion) it stays still. 8 places in the code switch motion off then.
Source
The file aiw add puts into your project. From then on it is yours.
Show the file, 497 lines
'use client';
import { useId, useState } from 'react';
import { cn, round, useInView, useLocale, useTexts } from '@/lib/kit';
import { Button } from '@/components/ui/button';
import { Icon, type IconName } from '@/components/ui/icon';
import { Eyebrow, Section } from '@/components/ui/section';
/*
* Awards, prizes and seals, every mark drawn here: laurels, round seals with
* text on the rim, a medal on a ribbon. No logos of juries or magazines, the
* name of the award stands as text. Five layouts:
*
* laurels a row of wreaths that draw themselves in, the award inside
* seals round seals with the issuer on the rim, the rim turns on hover
* trophy-case one medal set large, the others listed by year beside it
* by-year a year picker and the awards of that year as ribbons
* strip a compact line of seals and a rating, for next to a button
*/
export type AwardsVariant = 'laurels' | 'seals' | 'trophy-case' | 'by-year' | 'strip';
export interface AwardItem {
title: string;
/** Who gave it, written as text */
issuer?: string;
year?: string;
/** Gold, Winner, 1st place */
rank?: string;
category?: string;
description?: string;
icon?: IconName;
href?: string;
}
export interface AwardsProps {
variant?: AwardsVariant;
eyebrow?: React.ReactNode;
title?: React.ReactNode;
description?: React.ReactNode;
items: AwardItem[];
/** strip, trophy-case: an average rating */
rating?: { value: number; count: number; source?: string };
cta?: { label: string; href: string };
labels?: Partial<Texts>;
className?: string;
}
const TEXTS = {
en: { count: '{n} awards', count1: '1 award', year: 'Year', all: 'All years', winner: 'Winner', rating: '{v} out of 5', from: 'from {n} reviews', more: 'Read about it', awards: 'Awards' },
de: { count: '{n} Auszeichnungen', count1: '1 Auszeichnung', year: 'Jahr', all: 'Alle Jahre', winner: 'Gewinner', rating: '{v} von 5', from: 'aus {n} Bewertungen', more: 'Mehr dazu', awards: 'Auszeichnungen' },
};
type Texts = (typeof TEXTS)['en'];
type ViewProps = AwardsProps & { t: Texts };
const fill = (s: string, v: Record<string, string | number>) => s.replace(/\{(\w+)\}/g, (_, k) => String(v[k] ?? ''));
const delay = (ms: number) => ({ '--kit-delay': `${ms}ms` }) as React.CSSProperties;
function Head({ eyebrow, title, description, center, className }: Pick<AwardsProps, 'eyebrow' | 'title' | 'description'> & { center?: boolean; className?: string }) {
if (!title && !eyebrow) return null;
return (
<div className={cn('max-w-[46rem]', center && 'mx-auto max-w-[42rem] text-center', className)}>
{eyebrow && <Eyebrow>{eyebrow}</Eyebrow>}
{title && <h2 className="kit-rise text-balance text-h2 font-semibold text-foreground">{title}</h2>}
{description && (
<p className={cn('kit-rise mt-5 max-w-[36rem] text-pretty text-lead text-muted-foreground', center && 'mx-auto')} style={delay(80)}>
{description}
</p>
)}
</div>
);
}
/* ------------------------------------------------------------------ */
/* Drawn marks */
/* ------------------------------------------------------------------ */
/** One branch of a laurel: leaves along an arc, mirrored for the other side */
function Branch({ flip, drawn, leaves = 9 }: { flip?: boolean; drawn: boolean; leaves?: number }) {
// the left half of a wreath: from the bottom (252 deg) up the side to the top (112 deg)
const cx = 60;
const cy = 56;
const r = 44;
const from = 252;
const to = 112;
const at = (deg: number) => {
const a = (deg * Math.PI) / 180;
return [cx + r * Math.cos(a), cy - r * Math.sin(a)] as const;
};
const stem = Array.from({ length: 25 }, (_, i) => at(from - ((from - to) * i) / 24))
.map(([x, y], i) => `${i ? 'L' : 'M'}${round(x)} ${round(y)}`)
.join(' ');
const items = Array.from({ length: leaves * 2 }, (_, k) => {
const i = Math.floor(k / 2);
const out = k % 2 === 0;
const deg = from - 8 - ((from - to - 14) * i) / (leaves - 1);
const a = (deg * Math.PI) / 180;
// direction of travel along the stem (upward), as the rotation of an upright leaf
const along = (Math.atan2(Math.sin(a), -Math.cos(a)) * 180) / Math.PI;
const sc = 1 - i * 0.05;
const off = (out ? 6 : -5) * sc;
const [px, py] = at(deg);
return { x: round(px + off * Math.cos(a)), y: round(py - off * Math.sin(a)), rot: round(along + (out ? -38 : 38)), s: round(sc, 3), i };
});
return (
<g transform={flip ? 'translate(120 0) scale(-1 1)' : undefined}>
<path d={stem} fill="none" stroke="currentColor" strokeWidth="1.3" strokeLinecap="round" pathLength={1} strokeDasharray="1" strokeDashoffset={drawn ? 0 : 1} className="transition-[stroke-dashoffset] duration-[1200ms] ease-[var(--ease-kit)] motion-reduce:transition-none" />
{items.map((l, k) => (
<ellipse
key={k}
cx={l.x}
cy={l.y}
rx={round(2.6 * l.s)}
ry={round(6.4 * l.s)}
transform={`rotate(${l.rot} ${l.x} ${l.y})`}
fill="currentColor"
className="transition-opacity duration-500 ease-[var(--ease-kit)] motion-reduce:transition-none"
style={{ opacity: drawn ? round(0.95 - l.i * 0.04, 2) : 0, transitionDelay: `${200 + l.i * 80}ms` }}
/>
))}
</g>
);
}
function Laurel({ drawn, className }: { drawn: boolean; className?: string }) {
return (
<svg viewBox="0 0 120 110" aria-hidden className={cn('text-[color-mix(in_oklab,var(--primary)_65%,var(--foreground))]', className)}>
<Branch drawn={drawn} />
<Branch drawn={drawn} flip />
</svg>
);
}
/** A round seal with text on the rim. The rim turns slowly on hover. */
function Seal({ top, bottom, center, icon, size = 168, className }: { top: string; bottom?: string; center?: string; icon?: IconName; size?: number; className?: string }) {
const id = useId().replace(/:/g, '');
const teeth = 36;
const star = Array.from({ length: teeth * 2 }, (_, i) => {
const a = (i * Math.PI) / teeth;
const r = i % 2 ? 46.5 : 49;
return `${round(50 + r * Math.cos(a))},${round(50 + r * Math.sin(a))}`;
}).join(' ');
// the rim text runs over the top on an arc of 240 degrees and shrinks to fit long issuer names
const rimSize = Math.min(6.4, round(145 / (0.85 * Math.max(1, top.length)), 2));
const long = top.length > 18;
return (
<span className={cn('group/seal relative inline-grid place-items-center', className)} style={{ width: size, height: size }}>
<svg viewBox="0 0 100 100" aria-hidden data-clip-ok className="absolute inset-0 size-full">
<defs>
<path id={`${id}-t`} d="M 18.82 68 A 36 36 0 1 1 81.18 68" />
<path id={`${id}-b`} d="M 50 50 m -39.5 0 a 39.5 39.5 0 0 0 79 0" />
</defs>
<polygon points={star} className="fill-[color-mix(in_oklab,var(--primary)_14%,var(--card))] stroke-[color-mix(in_oklab,var(--primary)_40%,var(--border))]" strokeWidth="0.6" />
<g className="origin-center transition-transform duration-[2400ms] ease-[var(--ease-kit)] group-hover/seal:rotate-[24deg] motion-reduce:transition-none">
<circle cx="50" cy="50" r="42" fill="none" className="stroke-[color-mix(in_oklab,var(--primary)_55%,var(--foreground))]" strokeWidth="0.5" />
<circle cx="50" cy="50" r="31" fill="none" className="stroke-[color-mix(in_oklab,var(--primary)_55%,var(--foreground))]" strokeWidth="0.5" />
<text fontSize={rimSize} fontWeight="600" letterSpacing={round(rimSize * 0.17, 2)} className="fill-foreground uppercase">
<textPath href={`#${id}-t`} startOffset="50%" textAnchor="middle">
{top}
</textPath>
</text>
{bottom && (
<text fontSize="5.6" fontWeight="500" letterSpacing="1" className="fill-muted-foreground uppercase">
<textPath href={`#${id}-b`} startOffset="50%" textAnchor="middle" dominantBaseline="hanging">
{bottom}
</textPath>
</text>
)}
{!long && [0, 180].map((a) => (
<circle key={a} cx={round(50 + 36.5 * Math.cos(((a + 0) * Math.PI) / 180))} cy="50" r="0.9" className="fill-[color-mix(in_oklab,var(--primary)_60%,var(--foreground))]" />
))}
</g>
</svg>
<span className="relative flex max-w-[52%] flex-col items-center text-center">
{icon && <Icon name={icon} size={Math.round(size / 7.5)} className="text-[color-mix(in_oklab,var(--primary)_70%,var(--foreground))]" />}
{center && <span className={cn('font-semibold leading-tight tracking-[-0.02em] text-foreground', icon ? 'mt-1' : '')} style={{ fontSize: Math.max(11, Math.round(size / 11)) }}>{center}</span>}
</span>
</span>
);
}
/** A medal on a ribbon in the accent hue, at fixed lightness so the white text reads in both modes and with every accent */
function Medal({ label, sub, className }: { label: string; sub?: string; className?: string }) {
const id = useId().replace(/:/g, '');
return (
<svg viewBox="0 0 200 260" aria-hidden data-clip-ok className={className}>
<defs>
<linearGradient id={`${id}-m`} x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stopColor="oklch(from var(--primary) 0.74 c h)" />
<stop offset="0.5" stopColor="oklch(from var(--primary) 0.6 c h)" />
<stop offset="1" stopColor="oklch(from var(--primary) 0.42 c h)" />
</linearGradient>
<linearGradient id={`${id}-shine`} x1="0" y1="0" x2="1" y2="0">
<stop offset="0" stopColor="white" stopOpacity="0" />
<stop offset="0.5" stopColor="white" stopOpacity="0.45" />
<stop offset="1" stopColor="white" stopOpacity="0" />
</linearGradient>
<clipPath id={`${id}-c`}>
<circle cx="100" cy="170" r="70" />
</clipPath>
</defs>
{/* ribbon */}
<path d="M62 0h34l20 110H82Z" fill="oklch(from var(--primary) 0.45 c h)" />
<path d="M104 0h34l-20 110H84Z" fill="oklch(from var(--primary) 0.66 c h)" />
<path d="M78 96h44v18H78Z" fill="color-mix(in oklab, var(--foreground) 30%, var(--card))" />
{/* medal */}
<circle cx="100" cy="170" r="72" fill="color-mix(in oklab, var(--primary) 40%, var(--foreground))" opacity="0.35" />
<circle cx="100" cy="170" r="70" fill={`url(#${id}-m)`} />
<circle cx="100" cy="170" r="56" fill="none" stroke="rgb(255 255 255 / 0.5)" strokeWidth="1.2" strokeDasharray="2 3" />
<g clipPath={`url(#${id}-c)`}>
<rect x="-60" y="90" width="50" height="170" fill={`url(#${id}-shine)`} transform="skewX(-18)" className="animate-[aw-shine_5s_var(--ease-kit)_1s_infinite] motion-reduce:hidden" />
</g>
<text x="100" y={sub ? 172 : 180} textAnchor="middle" fontSize="26" fontWeight="700" fill="white" letterSpacing="-0.5">
{label}
</text>
{sub && (
<text x="100" y="196" textAnchor="middle" fontSize="11" fontWeight="600" letterSpacing="2" fill="white" opacity="0.85" className="uppercase">
{sub}
</text>
)}
<style>{'@keyframes aw-shine{0%{transform:skewX(-18deg) translateX(0)}40%,100%{transform:skewX(-18deg) translateX(260px)}}'}</style>
</svg>
);
}
function Stars({ value, size = 15 }: { value: number; size?: number }) {
return (
<span className="flex gap-0.5" aria-hidden>
{Array.from({ length: 5 }, (_, i) => {
const f = Math.max(0, Math.min(1, value - i));
return (
<span key={i} className="relative" style={{ width: size, height: size }}>
<Icon name="star" size={size} className="absolute inset-0 text-[color-mix(in_oklab,var(--foreground)_22%,transparent)]" />
<span data-clip-ok className="absolute inset-0 overflow-hidden" style={{ width: `${round(f * 100)}%` }}>
<Icon name="star" size={size} className="fill-current text-[color-mix(in_oklab,var(--warning)_85%,var(--foreground))]" />
</span>
</span>
);
})}
</span>
);
}
/* ------------------------------------------------------------------ */
/* laurels */
/* ------------------------------------------------------------------ */
function Laurels(p: ViewProps) {
const [ref, inView] = useInView<HTMLUListElement>();
const n = p.items.length;
return (
<Section className={p.className}>
<Head {...p} center />
<ul ref={ref} className={cn('mx-auto grid grid-cols-2 gap-x-4 gap-y-10 sm:gap-x-8', p.title && 'mt-14', n >= 4 ? 'lg:grid-cols-4' : n === 3 ? 'md:grid-cols-3' : '', 'max-w-[68rem]')}>
{p.items.map((it, i) => {
const inner = (
<>
<span className="relative grid aspect-[12/11] w-full max-w-[14rem] place-items-center">
<Laurel drawn={inView} className="absolute inset-0 size-full" />
<span className="relative flex max-w-[50%] flex-col items-center pt-2 text-center">
<span className="text-balance text-[13px] font-semibold leading-tight tracking-[-0.01em] text-foreground sm:text-[15px] lg:text-[16px]">{it.title}</span>
{it.year && <span className="mt-1.5 text-[12.5px] tabular-nums text-muted-foreground">{it.year}</span>}
</span>
</span>
{it.rank && <span className="mt-2 text-center text-[11px] font-semibold uppercase tracking-[0.14em] text-[color-mix(in_oklab,var(--primary)_75%,var(--muted-foreground))]">{it.rank}</span>}
{it.issuer && <span className="mt-1 text-center text-[13px] text-muted-foreground">{it.issuer}</span>}
</>
);
return (
<li key={i} className={cn('flex flex-col items-center transition-[opacity,transform] duration-700 ease-[var(--ease-kit)] motion-reduce:transition-none', inView ? 'opacity-100' : 'translate-y-3 opacity-0')} style={{ transitionDelay: `${i * 110}ms` }}>
{it.href ? (
<a href={it.href} className="group/l flex w-full cursor-pointer flex-col items-center rounded-2xl p-2 transition-transform duration-300 ease-[var(--ease-kit)] hover:-translate-y-1 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-solid focus-visible:outline-primary motion-reduce:hover:translate-y-0">
{inner}
</a>
) : (
<div className="flex w-full flex-col items-center p-2">{inner}</div>
)}
</li>
);
})}
</ul>
</Section>
);
}
/* ------------------------------------------------------------------ */
/* seals */
/* ------------------------------------------------------------------ */
function Seals(p: ViewProps) {
return (
<Section background="dots" className={p.className}>
<Head {...p} center />
<ul className={cn('mx-auto grid max-w-[64rem] grid-cols-2 gap-6 sm:gap-10 lg:grid-cols-4', p.title && 'mt-14')}>
{p.items.map((it, i) => (
<li key={i} className="kit-rise flex flex-col items-center text-center" style={delay(i * 90)}>
<Seal top={it.issuer ?? it.title} bottom={it.year} center={it.rank ?? it.title} icon={it.icon ?? 'award'} size={164} className="max-sm:!size-[8.5rem] transition-transform duration-500 ease-[var(--ease-kit)] hover:-rotate-3 motion-reduce:transition-none" />
<p className="mt-4 text-balance text-[14.5px] font-semibold text-foreground">{it.title}</p>
{it.category && <p className="mt-1 text-[13px] text-muted-foreground">{it.category}</p>}
</li>
))}
</ul>
</Section>
);
}
/* ------------------------------------------------------------------ */
/* trophy-case */
/* ------------------------------------------------------------------ */
function TrophyCase(p: ViewProps) {
const { t } = p;
const loc = useLocale() === 'de' ? 'de-DE' : 'en-US';
const [top, ...rest] = p.items;
if (!top) return null;
return (
<Section className={p.className}>
<div className="grid grid-cols-[minmax(0,1fr)] items-center gap-12 lg:grid-cols-[minmax(0,26rem)_minmax(0,1fr)] lg:gap-20">
<div className="kit-rise relative mx-auto w-full max-w-[22rem]">
<div aria-hidden className="absolute inset-x-6 bottom-0 top-1/3 rounded-full bg-[radial-gradient(closest-side,var(--glow),transparent)] blur-2xl" />
<Medal label={top.year ?? ''} sub={top.rank ?? t.winner} className="relative mx-auto w-[62%] animate-kit-float drop-shadow-[0_20px_30px_rgba(0,0,0,0.18)] motion-reduce:animate-none" />
<div className="relative mt-6 text-center">
<p className="text-balance text-[20px] font-semibold leading-snug text-foreground">{top.title}</p>
{top.issuer && <p className="mt-1 text-[14px] text-muted-foreground">{top.issuer}</p>}
{top.description && <p className="mx-auto mt-3 max-w-[22rem] text-pretty text-[14.5px] leading-relaxed text-muted-foreground">{top.description}</p>}
{top.href && (
<a href={top.href} className="mt-3 inline-flex min-h-11 cursor-pointer items-center gap-1.5 text-[14px] font-medium text-foreground underline-offset-4 hover:underline">
{t.more}
<Icon name="arrow-right" size={15} />
</a>
)}
</div>
</div>
<div>
<Head {...p} />
{p.rating && (
<p className="kit-rise mt-6 flex flex-wrap items-center gap-3 text-[14px] text-muted-foreground" style={delay(120)}>
<Stars value={p.rating.value} />
<span>
<strong className="font-semibold text-foreground">{fill(t.rating, { v: new Intl.NumberFormat(loc, { minimumFractionDigits: 1 }).format(p.rating.value) })}</strong> {fill(t.from, { n: new Intl.NumberFormat(loc).format(p.rating.count) })}
</span>
</p>
)}
<ul className="mt-10 border-t border-border">
{rest.map((it, i) => (
<li key={i} className="kit-rise grid grid-cols-[3.5rem_minmax(0,1fr)] items-center gap-4 border-b border-border py-4 sm:grid-cols-[4.5rem_minmax(0,1fr)_auto]" style={delay(160 + i * 60)}>
<span className="text-[15px] font-semibold tabular-nums text-muted-foreground">{it.year}</span>
<span className="min-w-0">
<span className="block text-pretty text-[15px] font-semibold text-foreground">{it.title}</span>
{it.issuer && <span className="block text-[13.5px] text-muted-foreground">{it.issuer}</span>}
</span>
{it.rank && <span className="col-start-2 inline-flex h-7 w-fit items-center gap-1.5 rounded-full border border-border px-3 text-[12.5px] font-medium text-foreground sm:col-start-3"><Icon name="award" size={14} className="text-primary" />{it.rank}</span>}
</li>
))}
</ul>
{p.cta && (
<Button href={p.cta.href} variant="outline" icon="arrow-right" className="mt-8 max-sm:w-full">
{p.cta.label}
</Button>
)}
</div>
</div>
</Section>
);
}
/* ------------------------------------------------------------------ */
/* by-year */
/* ------------------------------------------------------------------ */
function ByYear(p: ViewProps) {
const { t } = p;
const id = useId();
const years = [...new Set(p.items.map((i) => i.year ?? ''))].filter(Boolean).sort((a, b) => b.localeCompare(a));
const [year, setYear] = useState(years[0] ?? '');
const list = p.items.filter((i) => i.year === year);
return (
<Section background="surface" className={p.className}>
<div className="grid grid-cols-[minmax(0,1fr)] gap-10 lg:grid-cols-[minmax(0,1fr)_auto] lg:items-end">
<Head {...p} />
<div
role="tablist"
aria-label={t.year}
className="kit-rise flex gap-1 overflow-x-auto rounded-xl border border-border bg-card p-1 kit-no-scrollbar max-lg:w-fit max-lg:max-w-full"
onKeyDown={(e) => {
const d = e.key === 'ArrowRight' ? 1 : e.key === 'ArrowLeft' ? -1 : 0;
if (!d) return;
e.preventDefault();
const next = (years.indexOf(year) + d + years.length) % years.length;
setYear(years[next]);
(e.currentTarget.querySelectorAll('[role="tab"]')[next] as HTMLElement | undefined)?.focus();
}}
>
{years.map((y) => (
<button
key={y}
type="button"
role="tab"
id={`${id}-${y}`}
aria-selected={year === y}
aria-controls={`${id}-p`}
tabIndex={year === y ? 0 : -1}
onClick={() => setYear(y)}
className={cn('h-11 shrink-0 cursor-pointer rounded-lg px-4 text-[14px] font-semibold tabular-nums transition-[background-color,color] duration-300 focus-visible:outline-2 focus-visible:outline-solid focus-visible:outline-primary sm:h-10', year === y ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:bg-surface hover:text-foreground')}
>
{y}
</button>
))}
</div>
</div>
<div id={`${id}-p`} role="tabpanel" aria-labelledby={`${id}-${year}`} className="mt-12 grid grid-cols-[minmax(0,1fr)] gap-6 lg:grid-cols-[minmax(0,14rem)_minmax(0,1fr)] lg:gap-10">
{/* the chosen year, set large, with the count below */}
<div key={year} className="kit-rise flex items-baseline gap-4 lg:block">
<p className="text-[clamp(3.5rem,2.8rem+3vw,6rem)] font-semibold leading-none tracking-[-0.05em] text-foreground tabular-nums">{year}</p>
<p className="text-[14px] text-muted-foreground lg:mt-3">{fill(list.length === 1 ? t.count1 : t.count, { n: list.length })}</p>
</div>
<ul className="relative grid grid-cols-[minmax(0,1fr)] gap-3 md:grid-cols-2">
{list.map((it, i) => (
<li key={`${year}-${i}`} className="kit-rise relative flex gap-4 overflow-hidden rounded-2xl border border-border bg-card p-5 pl-6 shadow-[var(--highlight),var(--shadow-sm)]" style={delay(i * 80)}>
{/* a ribbon tail on the left edge */}
<span aria-hidden className="absolute inset-y-0 left-0 w-1.5 bg-primary" />
<span className="grid size-11 shrink-0 place-items-center rounded-full bg-[color-mix(in_oklab,var(--primary)_12%,var(--card))] text-[color-mix(in_oklab,var(--primary)_80%,var(--foreground))]">
<Icon name={it.icon ?? 'trophy'} size={20} />
</span>
<div className="min-w-0">
{it.rank && <p className="text-[12px] font-semibold uppercase tracking-[0.12em] text-[color-mix(in_oklab,var(--primary)_75%,var(--muted-foreground))]">{it.rank}</p>}
<p className="mt-0.5 text-pretty text-[16px] font-semibold leading-snug text-foreground">{it.title}</p>
<p className="mt-1 text-[13.5px] text-muted-foreground">
{[it.issuer, it.category].filter(Boolean).join(' · ')}
</p>
{it.description && <p className="mt-2 text-pretty text-[14px] leading-relaxed text-muted-foreground">{it.description}</p>}
</div>
</li>
))}
</ul>
</div>
</Section>
);
}
/* ------------------------------------------------------------------ */
/* strip */
/* ------------------------------------------------------------------ */
function Strip(p: ViewProps) {
const { t } = p;
const locale = useLocale();
return (
<div className={cn('py-10', p.className)}>
<div className="kit-container">
<div className="flex flex-col items-center gap-6 rounded-2xl border border-border bg-card px-5 py-6 shadow-[var(--highlight),var(--shadow-sm)] sm:px-8 xl:flex-row xl:justify-between">
{p.rating && (
<div className="kit-rise flex shrink-0 items-center gap-4 xl:border-r xl:border-border xl:pr-8">
<span className="text-[40px] font-semibold leading-none tabular-nums tracking-[-0.04em] text-foreground">{new Intl.NumberFormat(locale === 'de' ? 'de-DE' : 'en-US', { minimumFractionDigits: 1 }).format(p.rating.value)}</span>
<span>
<Stars value={p.rating.value} size={16} />
<span className="mt-1 block text-[13px] text-muted-foreground">
{fill(t.from, { n: new Intl.NumberFormat(locale === 'de' ? 'de-DE' : 'en-US').format(p.rating.count) })}
{p.rating.source && ` · ${p.rating.source}`}
</span>
<span className="sr-only">{fill(t.rating, { v: new Intl.NumberFormat(locale === 'de' ? 'de-DE' : 'en-US', { minimumFractionDigits: 1 }).format(p.rating.value) })}</span>
</span>
</div>
)}
<ul aria-label={t.awards} className="grid w-full grid-cols-2 gap-x-4 gap-y-5 sm:max-w-[40rem] sm:gap-x-8 xl:flex xl:w-auto xl:max-w-none xl:flex-nowrap xl:gap-8">
{p.items.map((it, i) => (
<li key={i} className="kit-rise flex items-center gap-3" style={delay(80 + i * 70)}>
<Seal top={it.issuer ?? ''} center={it.year} size={56} className="shrink-0 [&_text]:hidden" icon={undefined} />
<span className="min-w-0">
<span className="block text-pretty text-[13.5px] font-semibold leading-tight text-foreground">{it.title}</span>
{it.issuer && <span className="mt-0.5 block text-[12px] text-muted-foreground">{it.issuer}</span>}
</span>
</li>
))}
</ul>
</div>
</div>
</div>
);
}
/* ------------------------------------------------------------------ */
const VIEWS: Record<AwardsVariant, (p: ViewProps) => React.ReactNode> = {
laurels: Laurels,
seals: Seals,
'trophy-case': TrophyCase,
'by-year': ByYear,
strip: Strip,
};
export function Awards({ variant = 'laurels', labels, ...rest }: AwardsProps) {
const t = useTexts(TEXTS, labels);
const View = VIEWS[variant] ?? Laurels;
return <View {...rest} variant={variant} t={t} />;
}Frequent questions
How do I install Awards?
With aiw add @ai-website/awards. The command puts components/ui/awards.tsx into your project, the code is yours from then on. It brings along button, icon and section. It needs no further npm packages.
Which variants does Awards have?
5 variants, Laurels, Seals, Trophy case, By year and Compact strip. You pick one with the variant prop, for example variant="seals".
Can I translate the texts of Awards?
Yes. Awards carries 9 texts of its own in English and German, such as “1 award”, “Year” and “All years”. The language follows the KitProvider, single texts change through the labels prop.
Can an AI assistant build with Awards?
Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Awards with aiw_components and install it with aiw_add @ai-website/awards. Name the variant in your prompt, for example variant="laurels", and the assistant writes your content straight into the props.