Skip to content

Commit 57bfc75

Browse files
committed
feat(shimmer): add shimmer text utility as full component doc
- Add shimmer/shimmer-once/shimmer-reverse/shimmer-none/shimmer-color-*/shimmer-duration-*/shimmer-spread-*/shimmer-angle-* CSS utilities - Fix shimmer glyph fill to use -webkit-text-fill-color instead of color so it isn't overridden by companion text-* utilities - Add Motion section to Foundations page documenting the utility - Add full Shimmer component doc (src/lib/registry/shimmer.tsx) with Demo, With Marker, Color, Duration, Spread, Angle, Reverse, Play Once, Disabling, Fallback, Reduced Motion, RTL examples - Register shimmer in component-names and registry index so it appears in the Components sidebar - Add public/registry/care-ui/shimmer/shimmer.json registry:style item so the CLI install command resolves - Fix ExampleItem to default to the Code tab (and hide the Preview tab) when an example has no preview, fixing blank-looking code-only examples
1 parent c58e9f1 commit 57bfc75

7 files changed

Lines changed: 637 additions & 2 deletions

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"$schema": "https://ui.shadcn.com/schema/registry-item.json",
3+
"name": "shimmer",
4+
"type": "registry:style",
5+
"title": "Shimmer",
6+
"description": "Utilities for adding a shimmer effect to text elements.",
7+
"css": {
8+
"@keyframes tw-shimmer": {
9+
"from": { "background-position": "200% 0" },
10+
"to": { "background-position": "-200% 0" }
11+
},
12+
"@utility shimmer": {
13+
"--shimmer-color": "currentColor",
14+
"--shimmer-duration": "2s",
15+
"--shimmer-spread": "calc(3ch + 40px)",
16+
"--shimmer-angle": "20deg",
17+
"--shimmer-image": "linear-gradient(var(--shimmer-angle), transparent calc(50% - var(--shimmer-spread) / 2), color-mix(in oklch, var(--shimmer-color) 92%, transparent) 50%, transparent calc(50% + var(--shimmer-spread) / 2))",
18+
"--shimmer-text-fill": "color-mix(in oklch, currentColor 40%, transparent)",
19+
"background-repeat": "no-repeat",
20+
"background-image": "var(--shimmer-image), linear-gradient(var(--shimmer-text-fill), var(--shimmer-text-fill))",
21+
"background-size": "200% 100%, 100% 100%",
22+
"background-position": "200% 0, 0 0",
23+
"-webkit-background-clip": "text",
24+
"background-clip": "text",
25+
"-webkit-text-fill-color": "transparent",
26+
"animation": "tw-shimmer var(--shimmer-duration, 2s) linear infinite",
27+
"&:dir(rtl)": {
28+
"animation-direction": "reverse"
29+
}
30+
},
31+
"@utility shimmer-once": {
32+
"animation-iteration-count": "1"
33+
},
34+
"@utility shimmer-reverse": {
35+
"animation-direction": "reverse"
36+
},
37+
"@utility shimmer-none": {
38+
"background-image": "none",
39+
"-webkit-text-fill-color": "currentColor",
40+
"animation": "none"
41+
},
42+
"@utility shimmer-color-*": "--shimmer-color: --value(--color, [color]); --shimmer-color: color-mix(in oklch, --value(--color, [color]) --modifier(percentage), transparent);",
43+
"@utility shimmer-duration-*": {
44+
"--shimmer-duration": "calc(--value(integer) * 1ms)"
45+
},
46+
"@utility shimmer-spread-*": "--shimmer-spread: calc(var(--spacing) * --value(integer)); --shimmer-spread: --value([length], [percentage]);",
47+
"@utility shimmer-angle-*": {
48+
"--shimmer-angle": "calc(--value(integer) * 1deg)"
49+
},
50+
"@media (prefers-reduced-motion: reduce)": {
51+
".shimmer": {
52+
"animation": "none",
53+
"background-image": "none",
54+
"-webkit-text-fill-color": "currentColor"
55+
}
56+
}
57+
}
58+
}

src/components/dynamic-main-content.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ function PropsTable({
127127
}
128128

129129
function ExampleItem({ example }: { example: ComponentExample }) {
130-
const [tab, setTab] = useState("preview");
130+
const [tab, setTab] = useState(example.preview ? "preview" : "code");
131131
const { copyToClipboard, isCopied } = useCopyToClipboard();
132132

133133
return (
@@ -155,7 +155,9 @@ function ExampleItem({ example }: { example: ComponentExample }) {
155155
<Tabs value={tab} onValueChange={setTab} className="mt-4 w-full">
156156
<div className="flex items-center justify-between">
157157
<TabsList>
158-
<TabsTrigger value="preview">Preview</TabsTrigger>
158+
{example.preview && (
159+
<TabsTrigger value="preview">Preview</TabsTrigger>
160+
)}
159161
<TabsTrigger value="code">Code</TabsTrigger>
160162
</TabsList>
161163
{tab === "code" && (

src/components/foundations-page.tsx

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,44 @@ const RADIUS: RadiusRow[] = [
198198
},
199199
];
200200

201+
/* ── Shimmer utilities (custom Care UI, text highlight sweep) ── */
202+
203+
const SHIMMER_UTILITIES: { className: string; styles: string }[] = [
204+
{
205+
className: "shimmer",
206+
styles: "background-clip: text; animate tw-shimmer 2s linear infinite.",
207+
},
208+
{
209+
className: "shimmer-once",
210+
styles: "animation-iteration-count: 1.",
211+
},
212+
{
213+
className: "shimmer-reverse",
214+
styles: "animation-direction: reverse.",
215+
},
216+
{
217+
className: "shimmer-none",
218+
styles: "Disables the effect — renders the text normally.",
219+
},
220+
{
221+
className: "shimmer-color-<color>",
222+
styles: "Sets --shimmer-color, e.g. shimmer-color-blue-500/60.",
223+
},
224+
{
225+
className: "shimmer-duration-<number>",
226+
styles: "Sweep duration in ms — default 2000.",
227+
},
228+
{
229+
className: "shimmer-spread-<number>",
230+
styles: "Highlight band width via the spacing scale.",
231+
},
232+
{
233+
className: "shimmer-angle-<number>",
234+
styles: "Tilt of the highlight band in degrees — default 20.",
235+
},
236+
];
237+
238+
201239
/* ── Squircle utilities (custom Care UI, requires corner-shape support) ── */
202240

203241
const SQUIRCLE: { className: string; based_on: string; note: string }[] = [
@@ -615,6 +653,66 @@ export function FoundationsPage() {
615653
</div>
616654
</section>
617655

656+
{/* Motion */}
657+
<section>
658+
<SectionHeading id="motion">Motion</SectionHeading>
659+
<DocumentationParagraph>
660+
The <InlineCode>shimmer</InlineCode> utility (declared in{" "}
661+
<InlineCode>src/index.css</InlineCode>) animates a highlight sweep
662+
across text via <InlineCode>background-clip: text</InlineCode>. It is
663+
built on <InlineCode>currentColor</InlineCode>, so it adapts to any
664+
text color with no configuration, and brightens automatically in
665+
dark mode. Use it for streaming/generating states —{" "}
666+
<InlineCode>Generating response…</InlineCode> — instead of a plain
667+
pulse or opacity animation.
668+
</DocumentationParagraph>
669+
670+
<div className="bg-muted/40 mt-6 flex flex-col items-center justify-center gap-3 rounded-lg p-10">
671+
<p className="shimmer text-2xl font-semibold">
672+
Generating response&hellip;
673+
</p>
674+
<p className="shimmer shimmer-color-primary text-sm">
675+
Thinking&hellip;
676+
</p>
677+
</div>
678+
679+
<div className="border-border mt-6 overflow-hidden rounded-lg border">
680+
<Table>
681+
<TableHeader>
682+
<TableRow>
683+
<TableHead>Class</TableHead>
684+
<TableHead>Styles</TableHead>
685+
</TableRow>
686+
</TableHeader>
687+
<TableBody>
688+
{SHIMMER_UTILITIES.map((s) => (
689+
<TableRow key={s.className}>
690+
<TableCell className="font-mono text-xs">
691+
{s.className}
692+
</TableCell>
693+
<TableCell className="text-muted-foreground text-sm">
694+
{s.styles}
695+
</TableCell>
696+
</TableRow>
697+
))}
698+
</TableBody>
699+
</Table>
700+
</div>
701+
702+
<div className="border-border bg-card mt-6 rounded-lg border p-6">
703+
<div className="text-foreground text-sm font-semibold">
704+
Reduced motion &amp; RTL
705+
</div>
706+
<p className="text-muted-foreground mt-2 text-sm leading-6">
707+
When the user prefers reduced motion, the animation is disabled
708+
automatically and the text renders normally — nothing to
709+
configure. The sweep also follows reading direction automatically
710+
(left-to-right in LTR, right-to-left in RTL); use{" "}
711+
<InlineCode>shimmer-reverse</InlineCode> to flip it manually.
712+
</p>
713+
</div>
714+
</section>
715+
618716
{/* Borders & Radius */}
619717
<section>
620718
<SectionHeading id="borders-radius">

src/index.css

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,95 @@
641641
}
642642
}
643643

644+
/* ── Shimmer utilities ──────────────────────────────────────────────────────
645+
Animated highlight sweep for text, built on `background-clip: text` with
646+
`currentColor` as the base — no configuration needed to match any brand
647+
or muted color, light or dark mode. */
648+
649+
@keyframes tw-shimmer {
650+
from {
651+
background-position: 200% 0;
652+
}
653+
to {
654+
background-position: -200% 0;
655+
}
656+
}
657+
658+
@utility shimmer {
659+
--shimmer-color: currentColor;
660+
--shimmer-duration: 2s;
661+
--shimmer-spread: calc(3ch + 40px);
662+
--shimmer-angle: 20deg;
663+
--shimmer-image: linear-gradient(
664+
var(--shimmer-angle),
665+
transparent calc(50% - var(--shimmer-spread) / 2),
666+
color-mix(in oklch, var(--shimmer-color) 92%, transparent) 50%,
667+
transparent calc(50% + var(--shimmer-spread) / 2)
668+
);
669+
--shimmer-text-fill: color-mix(in oklch, currentColor 40%, transparent);
670+
671+
background-repeat: no-repeat;
672+
background-image:
673+
var(--shimmer-image),
674+
linear-gradient(var(--shimmer-text-fill), var(--shimmer-text-fill));
675+
background-size: 200% 100%, 100% 100%;
676+
background-position: 200% 0, 0 0;
677+
-webkit-background-clip: text;
678+
background-clip: text;
679+
/* A separate property (not `color`) so it can't lose the cascade to a
680+
companion text-color utility like `text-muted-foreground`. */
681+
-webkit-text-fill-color: transparent;
682+
animation: tw-shimmer var(--shimmer-duration, 2s) linear infinite;
683+
684+
&:dir(rtl) {
685+
animation-direction: reverse;
686+
}
687+
}
688+
689+
@utility shimmer-once {
690+
animation-iteration-count: 1;
691+
}
692+
693+
@utility shimmer-reverse {
694+
animation-direction: reverse;
695+
}
696+
697+
@utility shimmer-none {
698+
background-image: none;
699+
-webkit-text-fill-color: currentColor;
700+
animation: none;
701+
}
702+
703+
@utility shimmer-color-* {
704+
--shimmer-color: --value(--color, [color]);
705+
--shimmer-color: color-mix(
706+
in oklch,
707+
--value(--color, [color]) --modifier(percentage),
708+
transparent
709+
);
710+
}
711+
712+
@utility shimmer-duration-* {
713+
--shimmer-duration: calc(--value(integer) * 1ms);
714+
}
715+
716+
@utility shimmer-spread-* {
717+
--shimmer-spread: calc(var(--spacing) * --value(integer));
718+
--shimmer-spread: --value([length], [percentage]);
719+
}
720+
721+
@utility shimmer-angle-* {
722+
--shimmer-angle: calc(--value(integer) * 1deg);
723+
}
724+
725+
@media (prefers-reduced-motion: reduce) {
726+
.shimmer {
727+
animation: none;
728+
background-image: none;
729+
-webkit-text-fill-color: currentColor;
730+
}
731+
}
732+
644733
/* ── Session Timeout Animations ──────────────────────────────────────────── */
645734
@keyframes ripple-pulse-slow {
646735
0% {

src/lib/component-names.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export const componentNames: Record<string, string> = {
33
attachment: "Attachment",
44
bubble: "Bubble",
55
marker: "Marker",
6+
shimmer: "Shimmer",
67
message: "Message",
78
accordion: "Accordion",
89
alert: "Alert",

src/lib/registry/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ const componentLoaders: Record<
3636
import("./attachment").then((m) => ({ default: m.attachmentDoc })),
3737
bubble: () => import("./bubble").then((m) => ({ default: m.bubbleDoc })),
3838
marker: () => import("./marker").then((m) => ({ default: m.markerDoc })),
39+
shimmer: () => import("./shimmer").then((m) => ({ default: m.shimmerDoc })),
3940
message: () => import("./message").then((m) => ({ default: m.messageDoc })),
4041
accordion: () =>
4142
import("./accordion").then((m) => ({ default: m.accordionDoc })),

0 commit comments

Comments
 (0)