Skip to content

Commit 81c38ab

Browse files
bloveclaude
andauthored
fix(website): make Card hover lift render border/shadow (+ accent ring) (#579)
* fix(website): make Card hover lift actually render border/shadow The global [data-hoverable]:hover rule set box-shadow/border-color without !important, so Card's inline border/box-shadow always won — only the translate applied. Add !important so the lift renders; add an accent variant (accent ring, keeps the accent border) via a new Card `accent` prop. Switch the docs landing + prev/next cards from the broken ad-hoc `data-ui="docs-card"` scoped styles to the shared hoverable/accent mechanism and delete the duplicated <style> blocks. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(website): drop !important — fix Card hover at the cascade root The previous fix beat Card's inline border/box-shadow with !important on the hover rules. That treats the symptom: the resting border/box-shadow are still inline, so every future state (focus, active) hits the same wall, and per-card overrides have to fight !important. Move Card's resting background/border/box-shadow/transition out of inline and into the `[data-ui="card"]` stylesheet; drive surface via `data-surface` and the accent fill via `[data-ui="card"][data-accent]`. With nothing inline, the hover rules win on specificity alone — no !important anywhere. Accent cards keep the accent ring + shadow; plain cards get the neutral border-strong + shadow lift. Verified: e2e/docs.spec.ts 8/8; real Playwright hover shows the accent ring (0 0 0 3px accent-glow + shadow-md) and the neutral lift render correctly. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5b6f768 commit 81c38ab

4 files changed

Lines changed: 51 additions & 37 deletions

File tree

apps/website/src/app/docs/page.tsx

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -285,13 +285,7 @@ const helperLinkStyle = {
285285
fontWeight: 600,
286286
} as const;
287287

288-
const accentCardStyle = {
289-
height: '100%',
290-
background: tokens.colors.accentSurface,
291-
border: `1px solid ${tokens.colors.accentBorder}`,
292-
} as const;
293-
294-
const plainCardStyle = {
288+
const fillHeightStyle = {
295289
height: '100%',
296290
} as const;
297291

@@ -305,11 +299,6 @@ const dividerStyle = {
305299
export default function DocsLandingPage() {
306300
return (
307301
<>
308-
<style>{`
309-
[data-ui="docs-card"] { transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease; }
310-
[data-ui="docs-card"]:hover { border-color: ${tokens.colors.accentBorderHover}; box-shadow: ${tokens.shadows.md}; transform: translateY(-1px); }
311-
@media (prefers-reduced-motion: reduce) { [data-ui="docs-card"]:hover { transform: none; } }
312-
`}</style>
313302

314303
{/* Hero */}
315304
<Section surface="canvas" ariaLabelledBy="docs-heading">
@@ -357,7 +346,7 @@ export default function DocsLandingPage() {
357346
<div style={gridStyle}>
358347
{BACKENDS.map((b) => (
359348
<Link key={b.href} href={b.href} style={{ textDecoration: 'none' }}>
360-
<Card padding="lg" data-ui="docs-card" style={accentCardStyle}>
349+
<Card padding="lg" hoverable accent style={fillHeightStyle}>
361350
<div style={cardHeaderStyle}>
362351
<LogoChip src={b.logoSrc} />
363352
<div>
@@ -392,7 +381,7 @@ export default function DocsLandingPage() {
392381
<div style={gridStyle}>
393382
{GENERATIVE_UI.map((g) => (
394383
<Link key={g.href} href={g.href} style={{ textDecoration: 'none' }}>
395-
<Card padding="lg" data-ui="docs-card" style={accentCardStyle}>
384+
<Card padding="lg" hoverable accent style={fillHeightStyle}>
396385
<div style={cardHeaderStyle}>
397386
<LogoChip src={g.logoSrc} />
398387
<div>
@@ -421,7 +410,7 @@ export default function DocsLandingPage() {
421410
<div style={dividerStyle} />
422411
<StepLabel id="chat-heading" step={3}>Chat UI</StepLabel>
423412
<Link href="/docs/chat/getting-started/introduction" style={{ textDecoration: 'none' }}>
424-
<Card padding="lg" data-ui="docs-card" style={plainCardStyle}>
413+
<Card padding="lg" hoverable style={fillHeightStyle}>
425414
<div style={cardHeaderStyle}>
426415
<GlyphChip size={30}><ChatGlyph /></GlyphChip>
427416
<div>
@@ -449,7 +438,7 @@ export default function DocsLandingPage() {
449438
const Glyph = GLYPHS[s.glyph];
450439
return (
451440
<Link key={s.href} href={s.href} style={{ textDecoration: 'none' }}>
452-
<Card padding="lg" data-ui="docs-card" style={plainCardStyle}>
441+
<Card padding="lg" hoverable style={fillHeightStyle}>
453442
<div style={{ display: 'flex', alignItems: 'center', gap: 12 }}>
454443
<GlyphChip size={26}><Glyph /></GlyphChip>
455444
<div>

apps/website/src/app/global.css

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,17 +188,44 @@ html {
188188
.docs-prose td { padding: 0.5rem 0.75rem; border-bottom: 1px solid rgba(0, 64, 144, 0.08); color: #555770; }
189189
.docs-prose td code { font-size: 0.8em; }
190190

191-
/* UI primitive — Card hover.
192-
* Hoverable cards are virtually always links; pointer is the right default.
191+
/* UI primitive — Card.
192+
* Resting background / border / shadow live here (not inline on the element)
193+
* so the :hover rules below can override border-color and box-shadow. Inline
194+
* styles beat any stylesheet :hover rule, which previously left the lift
195+
* rendering only the transform. See components/ui/Card.tsx. */
196+
[data-ui="card"] {
197+
background: var(--color-surface);
198+
border: 1px solid var(--color-border);
199+
box-shadow: var(--shadow-sm);
200+
transition: box-shadow 160ms ease, border-color 160ms ease, transform 160ms ease;
201+
}
202+
[data-ui="card"][data-surface="tinted"] {
203+
background: var(--color-surface-tinted);
204+
}
205+
[data-ui="card"][data-surface="dim"] {
206+
background: var(--color-surface-dim);
207+
}
208+
/* Accent-filled cards (e.g. docs backend / generative-UI cards). */
209+
[data-ui="card"][data-accent] {
210+
background: var(--color-accent-surface);
211+
border-color: var(--color-accent-border);
212+
}
213+
214+
/* Hoverable cards are virtually always links; pointer is the right default.
193215
* If a non-link gets `data-hoverable`, override inline. */
194216
[data-ui="card"][data-hoverable] {
195217
cursor: pointer;
196218
}
197-
[data-ui="card"][data-hoverable]:hover {
219+
[data-ui="card"][data-hoverable]:not([data-accent]):hover {
198220
box-shadow: var(--shadow-md);
199221
border-color: var(--color-border-strong);
200222
transform: translateY(-1px);
201223
}
224+
/* Accent cards keep their accent border and gain an accent ring on hover. */
225+
[data-ui="card"][data-hoverable][data-accent]:hover {
226+
box-shadow: 0 0 0 3px var(--color-accent-glow), var(--shadow-md);
227+
transform: translateY(-1px);
228+
}
202229
@media (prefers-reduced-motion: reduce) {
203230
[data-ui="card"][data-hoverable]:hover {
204231
transform: none;

apps/website/src/components/docs/DocsPrevNext.tsx

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,9 @@ export function DocsPrevNext({ library, section, slug }: Props) {
5555
marginBottom: 16,
5656
}}
5757
>
58-
<style>{`
59-
[data-ui="docs-card"] { transition: border-color 160ms ease, box-shadow 160ms ease, transform 160ms ease; }
60-
[data-ui="docs-card"]:hover { border-color: ${tokens.colors.accentBorderHover}; box-shadow: ${tokens.shadows.md}; transform: translateY(-1px); }
61-
@media (prefers-reduced-motion: reduce) { [data-ui="docs-card"]:hover { transform: none; } }
62-
`}</style>
6358
{prev ? (
6459
<Link href={prev.href} style={{ textDecoration: 'none' }}>
65-
<Card padding="md" data-ui="docs-card" style={{ height: '100%' }}>
60+
<Card padding="md" hoverable style={{ height: '100%' }}>
6661
<Eyebrow style={{ marginBottom: 8 }}>← Previous</Eyebrow>
6762
<div
6863
style={{
@@ -81,7 +76,7 @@ export function DocsPrevNext({ library, section, slug }: Props) {
8176
)}
8277
{next ? (
8378
<Link href={next.href} style={{ textDecoration: 'none' }}>
84-
<Card padding="md" data-ui="docs-card" style={{ height: '100%', textAlign: 'right' }}>
79+
<Card padding="md" hoverable style={{ height: '100%', textAlign: 'right' }}>
8580
<Eyebrow style={{ marginBottom: 8 }}>Next →</Eyebrow>
8681
<div
8782
style={{

apps/website/src/components/ui/Card.tsx

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,19 @@ type Padding = 'md' | 'lg';
77

88
interface CardProps extends HTMLAttributes<HTMLDivElement> {
99
children: ReactNode;
10-
/** If true, applies a subtle hover lift via CSS. */
10+
/** If true, applies a subtle hover lift (border + shadow + translate) via CSS. */
1111
hoverable?: boolean;
12+
/**
13+
* If true, renders an accent-tinted surface + border, and — when hoverable —
14+
* the hover lift uses an accent ring instead of the neutral border treatment.
15+
*/
16+
accent?: boolean;
1217
/** Internal padding tier. */
1318
padding?: Padding;
1419
/** Override the surface color. */
1520
surface?: Surface;
1621
}
1722

18-
const SURFACE: Record<Surface, string> = {
19-
white: tokens.surfaces.surface,
20-
tinted: tokens.surfaces.surfaceTinted,
21-
dim: tokens.surfaces.surfaceDim,
22-
};
23-
2423
const PADDING: Record<Padding, string> = {
2524
md: '20px',
2625
lg: '28px',
@@ -29,24 +28,28 @@ const PADDING: Record<Padding, string> = {
2928
export function Card({
3029
children,
3130
hoverable = false,
31+
accent = false,
3232
padding = 'md',
3333
surface = 'white',
3434
className,
3535
style,
3636
...rest
3737
}: CardProps) {
38+
// Resting background / border / shadow live in the `[data-ui="card"]`
39+
// stylesheet (apps/website/src/app/global.css) — never inline — so the
40+
// :hover rules can actually override border-color and box-shadow. Inline
41+
// styles beat any stylesheet :hover rule, which previously left the lift
42+
// rendering only the transform.
3843
return (
3944
<div
4045
data-ui="card"
4146
data-hoverable={hoverable || undefined}
47+
data-accent={accent || undefined}
48+
data-surface={surface !== 'white' ? surface : undefined}
4249
className={cn(className)}
4350
style={{
44-
background: SURFACE[surface],
45-
border: `1px solid ${tokens.surfaces.border}`,
4651
borderRadius: tokens.radius.lg,
47-
boxShadow: tokens.shadows.sm,
4852
padding: PADDING[padding],
49-
transition: 'box-shadow 160ms ease, border-color 160ms ease, transform 160ms ease',
5053
...style,
5154
}}
5255
{...rest}

0 commit comments

Comments
 (0)