Build with confidence.
Zero dependencies. Drop-in shadcn variable interop.
UI LIBRARY FOR DESIGN ENGINEERS
120 free and production-grade components, special effects, and animations built with pure CSS and zero runtime dependencies. Complete cross-UI design token interoperability.
Zero dependencies. Drop-in shadcn variable interop.
One connected foundation
120 / componentsConsidered down to the last detail
02 / themesBeautiful in a different light
00 / dependenciesAt runtime. Simply yours.
THE BUILDING BLOCKS
Familiar patterns, with a little more care.
Buttons & interactions
Forms & controls
Status & feedback
CONSISTENT BY NATURE
Color, type, space, and motion. Every decision has a place.
Change a token. Bring everything along.
Visible focus, native controls, readable contrast, and reduced motion preferences.
Local fonts for English, বাংলা, and العربية. Logical spacing for right-to-left layouts.
Plain HTML, CSS variables, and ES modules. Bring your favorite stack.
01 / THE FOUNDATION
A shared language for every surface, every state, and every little detail.
85 primitives. 48 semantic roles per theme. One consistent visual language.
Blue / Indigo / Violet
Components consume semantic aliases. Change a theme without rewriting a component.
| Semantic role | Light | Dark | Copy |
|---|---|---|---|
--s-bg-canvas | #FAFAFA | #09090B | |
--s-bg-surface | #FFFFFF | #121215 | |
--s-bg-subtle | #F4F4F5 | #18181B | |
--s-bg-hover | #E4E4E7 | #27272A | |
--s-bg-sunken | #E4E4E7 | #050506 | |
--s-bg-inverse | #09090B | #FFFFFF | |
--s-bg-disabled | #E4E4E7 | #27272A | |
--s-bg-brand | #F3F1FF | #211454 | |
--s-bg-overlay | #080B15A6 | #080B15A6 | |
--s-bg-transparent | #00000000 | #00000000 | |
--s-text-primary | #121215 | #F4F4F5 | |
--s-text-secondary | #52525B | #C4C4CC | |
--s-text-muted | #52525B | #A1A1AA | |
--s-text-disabled | #A1A1AA | #71717A | |
--s-text-inverse | #FFFFFF | #09090B | |
--s-text-brand | #4C2DDE | #BBA6FF | |
--s-text-link | #075ABE | #8CCDFF | |
--s-border-default | #D4D4D8 | #3F3F46 | |
--s-border-strong | #71717A | #A1A1AA | |
--s-border-focus | #5B3DF5 | #BBA6FF | |
--s-border-brand | #D7CDFF | #4026BA | |
--s-action-primary | #5B3DF5 | #BBA6FF | |
--s-action-primary-hover | #4C2DDE | #D7CDFF | |
--s-action-primary-pressed | #4026BA | #E9E4FF | |
--s-action-on-primary | #FFFFFF | #211454 | |
--s-action-secondary | #E4E4E7 | #27272A | |
--s-action-secondary-hover | #D4D4D8 | #3F3F46 | |
--s-action-danger | #B91C1C | #FCA5A5 | |
--s-action-danger-hover | #991B1B | #FECACA | |
--s-action-on-danger | #FFFFFF | #450A0A | |
--s-brand-blue | #178BFF | #4CADFF | |
--s-brand-indigo | #5B3DF5 | #BBA6FF | |
--s-brand-violet | #A020FF | #CC6BFF | |
--s-icon-primary | #3F3F46 | #D4D4D8 | |
--s-icon-muted | #71717A | #A1A1AA | |
--s-effect-shadow | #0D10201A | #0D10201A | |
--s-status-success-bg | #ECFDF5 | #022C22 | |
--s-status-success-text | #065F46 | #A7F3D0 | |
--s-status-success-border | #A7F3D0 | #065F46 | |
--s-status-warning-bg | #FFFBEB | #451A03 | |
--s-status-warning-text | #92400E | #FDE68A | |
--s-status-warning-border | #FDE68A | #92400E | |
--s-status-danger-bg | #FEF2F2 | #450A0A | |
--s-status-danger-text | #991B1B | #FECACA | |
--s-status-danger-border | #FECACA | #991B1B | |
--s-status-info-bg | #EFF8FF | #0B2853 | |
--s-status-info-text | #0D4B9B | #BDE2FF | |
--s-status-info-border | #BDE2FF | #0D4B9B |
Poppins for expression. Inter for clarity. Roboto Mono for precision.
Aa.
Confident. Considered. A little character.Aa.
Clarity in every character.Display/Hero64 / 76 · 600Make good things.
Display/Title48 / 60 · 600Make good things.
Heading/H140 / 52 · 600Make good things.
Heading/H232 / 44 · 600A little thought goes a long way.
Heading/H324 / 34 · 600A little thought goes a long way.
Heading/H420 / 30 · 500A little thought goes a long way.
Body/Large18 / 28 · 400A little thought goes a long way.
Body/Medium16 / 24 · 400A little thought goes a long way.
Body/Small14 / 22 · 400A little thought goes a long way.
Label/Large16 / 24 · 600A little thought goes a long way.
Label/Medium14 / 20 · 500A little thought goes a long way.
Label/Small12 / 18 · 500A little thought goes a long way.
Code/Medium13 / 22 · 400const possibilities = endless;
প্রতিটি ছোট বিবরণে থাকুক যত্ন ও নিজস্বতা।
كل تفصيل صغير يصنع تجربة أفضل.
An 8-based rhythm, with 2 and 4 for the smaller details.
A considered radius scale and three levels of elevation.
Quick feedback, gentle transitions, and respect for reduced motion.
180ms · cubic-bezier(.2, .8, .2, 1)
The foundation checks body and status contrast at 4.5:1 and focus at 3:1. Native controls, visible focus, reduced motion, and forced colors support accessible composition. Test your own product’s content and assistive technology experience too.
02 / THE COMPONENTS
120 considered building blocks. Real interactions, meaningful states, and a shared foundation.
Infinite smooth auto-scrolling marquee ticker with hover pause.
import { Marquee } from "@/components/sarahui/marquee"; export function MarqueeDemo() { return ( <div className="relative flex h-[300px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background md:shadow-xl"> <Marquee pauseOnHover className="[--duration:20s]"> <div className="flex gap-4"> <span className="rounded-lg border px-4 py-2 font-medium">Design Systems</span> <span className="rounded-lg border px-4 py-2 font-medium">Zero Dependencies</span> <span className="rounded-lg border px-4 py-2 font-medium">Adobe Spectrum Tokens</span> <span className="rounded-lg border px-4 py-2 font-medium">Progressive Enhancement</span> </div> </Marquee> </div> );}
Add Marquee to your project.
$ npx sarahui add marquee
import { Marquee } from 'sarahui';
<Marquee> <span>Next.js</span> <span>React</span> <span>TypeScript</span> <span>Tailwind CSS</span></Marquee>
import { Marquee } from "@/components/sarahui/marquee"; export function MarqueeDemoVertical() { return ( <div className="relative flex h-[500px] w-full flex-row items-center justify-center overflow-hidden"> <Marquee pauseOnHover vertical className="[--duration:20s]"> {firstRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))} </Marquee> <Marquee reverse pauseOnHover vertical className="[--duration:20s]"> {secondRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))} </Marquee> <div className="from-background pointer-events-none absolute inset-x-0 top-0 h-1/4 bg-linear-to-b"></div> <div className="from-background pointer-events-none absolute inset-x-0 bottom-0 h-1/4 bg-gradient-to-t"></div> </div> );}
import { Marquee } from "@/components/sarahui/marquee"; export function Marquee3D() { return ( <div className="relative flex h-96 w-full flex-row items-center justify-center gap-4 overflow-hidden [perspective:300px]"> <div className="flex flex-row items-center gap-4" style={{ transform: "translateX(-100px) translateY(0px) translateZ(-100px) rotateX(20deg) rotateY(-10deg) rotateZ(20deg)", }} > <Marquee pauseOnHover vertical className="[--duration:20s]"> {firstRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))} </Marquee> <Marquee reverse pauseOnHover vertical className="[--duration:20s]"> {secondRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))} </Marquee> <Marquee reverse pauseOnHover vertical className="[--duration:20s]"> {thirdRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))} </Marquee> <Marquee pauseOnHover vertical className="[--duration:20s]"> {fourthRow.map((review) => ( <ReviewCard key={review.username} {...review} /> ))} </Marquee> </div> <div className="from-background pointer-events-none absolute inset-x-0 top-0 h-1/4 bg-linear-to-b"></div> <div className="from-background pointer-events-none absolute inset-x-0 bottom-0 h-1/4 bg-gradient-to-t"></div> <div className="from-background pointer-events-none absolute inset-y-0 left-0 w-1/4 bg-gradient-to-r"></div> <div className="from-background pointer-events-none absolute inset-y-0 right-0 w-1/4 bg-gradient-to-l"></div> </div> );}
Configuration parameters and properties for Marquee.
| Prop | Type | Default | Description |
|---|---|---|---|
direction |
'left' | 'right' | 'up' | 'down' | 'left' |
Scroll direction of the marquee ticker track |
reverse |
boolean | false |
Whether to reverse the animation scroll direction |
pauseOnHover |
boolean | false |
Whether to pause ticker animation on mouse hover |
speed |
number | string | 30 |
Scroll animation speed in seconds or speed scale |
duration |
number | 30 |
Explicit duration override in seconds |
vertical |
boolean | false |
Whether ticker scrolls vertically instead of horizontally |
repeat |
number | 4 |
Number of track copies to duplicate for seamless looping |
gap |
number | string | '1rem' |
Spacing between adjacent items in the track |
className |
string | '' |
Optional CSS class for styling and layout overrides |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes to attach to root element |
children |
any | - |
Content elements to replicate inside the scrolling ticker |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Marquee({ children: UI.Badge({ label: "SarahUI Infinite Marquee" }) }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;display:flex;width:100%;flex-direction:column;align-items:center;justify-content:center;overflow:hidden;padding:16px 0;"> <div class="s-marquee group s-marquee--pause" style="--duration:20s;--gap:1rem;--s-gap:1rem;" data-s-marquee><div class="s-marquee-track"> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jack" src="assets/avatar-jack.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jack</figcaption> <p class="s-review-card-username">@jack</p> </div> </div> <blockquote class="s-review-card-body">I've never seen anything like this before. It's amazing. I love it.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jill" src="assets/avatar-jill.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jill</figcaption> <p class="s-review-card-username">@jill</p> </div> </div> <blockquote class="s-review-card-body">I don't know what to say. I'm speechless. This is amazing.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="John" src="assets/avatar-john.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">John</figcaption> <p class="s-review-card-username">@john</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> </div><div class="s-marquee-track" aria-hidden="true"> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jack" src="assets/avatar-jack.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jack</figcaption> <p class="s-review-card-username">@jack</p> </div> </div> <blockquote class="s-review-card-body">I've never seen anything like this before. It's amazing. I love it.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jill" src="assets/avatar-jill.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jill</figcaption> <p class="s-review-card-username">@jill</p> </div> </div> <blockquote class="s-review-card-body">I don't know what to say. I'm speechless. This is amazing.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="John" src="assets/avatar-john.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">John</figcaption> <p class="s-review-card-username">@john</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> </div><div class="s-marquee-track" aria-hidden="true"> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jack" src="assets/avatar-jack.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jack</figcaption> <p class="s-review-card-username">@jack</p> </div> </div> <blockquote class="s-review-card-body">I've never seen anything like this before. It's amazing. I love it.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jill" src="assets/avatar-jill.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jill</figcaption> <p class="s-review-card-username">@jill</p> </div> </div> <blockquote class="s-review-card-body">I don't know what to say. I'm speechless. This is amazing.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="John" src="assets/avatar-john.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">John</figcaption> <p class="s-review-card-username">@john</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> </div><div class="s-marquee-track" aria-hidden="true"> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jack" src="assets/avatar-jack.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jack</figcaption> <p class="s-review-card-username">@jack</p> </div> </div> <blockquote class="s-review-card-body">I've never seen anything like this before. It's amazing. I love it.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jill" src="assets/avatar-jill.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jill</figcaption> <p class="s-review-card-username">@jill</p> </div> </div> <blockquote class="s-review-card-body">I don't know what to say. I'm speechless. This is amazing.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="John" src="assets/avatar-john.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">John</figcaption> <p class="s-review-card-username">@john</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> </div></div> <div class="s-marquee group s-marquee--reverse s-marquee--pause" style="--duration:20s;--gap:1rem;--s-gap:1rem;" data-s-marquee><div class="s-marquee-track"> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jane" src="assets/avatar-jane.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jane</figcaption> <p class="s-review-card-username">@jane</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jenny" src="assets/avatar-jenny.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jenny</figcaption> <p class="s-review-card-username">@jenny</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="James" src="assets/avatar-james.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">James</figcaption> <p class="s-review-card-username">@james</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> </div><div class="s-marquee-track" aria-hidden="true"> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jane" src="assets/avatar-jane.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jane</figcaption> <p class="s-review-card-username">@jane</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jenny" src="assets/avatar-jenny.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jenny</figcaption> <p class="s-review-card-username">@jenny</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="James" src="assets/avatar-james.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">James</figcaption> <p class="s-review-card-username">@james</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> </div><div class="s-marquee-track" aria-hidden="true"> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jane" src="assets/avatar-jane.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jane</figcaption> <p class="s-review-card-username">@jane</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jenny" src="assets/avatar-jenny.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jenny</figcaption> <p class="s-review-card-username">@jenny</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="James" src="assets/avatar-james.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">James</figcaption> <p class="s-review-card-username">@james</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> </div><div class="s-marquee-track" aria-hidden="true"> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jane" src="assets/avatar-jane.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jane</figcaption> <p class="s-review-card-username">@jane</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="Jenny" src="assets/avatar-jenny.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">Jenny</figcaption> <p class="s-review-card-username">@jenny</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> <figure class="s-review-card"> <div style="display:flex;flex-direction:row;align-items:center;gap:8px;"> <img class="s-review-card-avatar" width="32" height="32" alt="James" src="assets/avatar-james.svg"/> <div style="display:flex;flex-direction:column;"> <figcaption class="s-review-card-name">James</figcaption> <p class="s-review-card-username">@james</p> </div> </div> <blockquote class="s-review-card-body">I'm at a loss for words. This is amazing. I love it.</blockquote> </figure> </div></div> <div style="pointer-events:none;position:absolute;inset-block:0;left:0;width:25%;background:linear-gradient(to right, var(--s-bg-canvas, var(--s-bg-surface)), transparent);z-index:2;"></div> <div style="pointer-events:none;position:absolute;inset-block:0;right:0;width:25%;background:linear-gradient(to left, var(--s-bg-canvas, var(--s-bg-surface)), transparent);z-index:2;"></div> </div> Interactive macOS styled code terminal with typing command execution.
✔ Preflight checks.
✔ Verifying framework. Found Next.js.
✔ Validating Tailwind CSS.
✔ Validating import alias.
✔ Writing components.json.
✔ Checking registry.
✔ Updating tailwind.config.ts
✔ Updating app/globals.css
✔ Installing dependencies.
ℹ Updated 1 file:- lib/utils.ts
import { AnimatedSpan, Terminal, TypingAnimation,} from "@/components/sarahui/terminal"; export function TerminalDemo() { return ( <Terminal> <TypingAnimation>> npx sarahui init</TypingAnimation> <AnimatedSpan className="text-green-500"> ✔ Preflight checks. </AnimatedSpan> <AnimatedSpan className="text-green-500"> ✔ Verifying framework configuration. </AnimatedSpan> <AnimatedSpan className="text-green-500"> ✔ Validating Tailwind CSS preset. </AnimatedSpan> <AnimatedSpan className="text-green-500"> ✔ 319 design tokens compiled. </AnimatedSpan> <AnimatedSpan className="text-green-500"> ✔ Writing sarahui.config.json. </AnimatedSpan> <TypingAnimation className="text-muted-foreground"> Success! Project initialization completed. </TypingAnimation> <TypingAnimation className="text-muted-foreground"> You may now add components using npx sarahui add <component>. </TypingAnimation> </Terminal> );}
Add Terminal to your project.
$ npx sarahui add terminal
import { Terminal } from 'sarahui';
<Terminal> <TypingAnimation>npx sarahui init</TypingAnimation> <AnimatedSpan>✔ Preflight checks.</AnimatedSpan> <AnimatedSpan>✔ Validating Tailwind CSS.</AnimatedSpan> <TypingAnimation>Success! Project initialization completed.</TypingAnimation></Terminal>
Documents Downloads Pictures
/home/user/Documents
import { AnimatedSpan, Terminal, TypingAnimation,} from "@/components/sarahui/terminal"; export function TerminalCustomDelays() { return ( <Terminal> <TypingAnimation delay={0}>$ ls</TypingAnimation> <AnimatedSpan delay={800} className="text-blue-500"> Documents Downloads Pictures </AnimatedSpan> <TypingAnimation delay={1600}>$ cd Documents</TypingAnimation> <TypingAnimation delay={2400}>$ pwd</TypingAnimation> <AnimatedSpan delay={3200} className="text-green-500"> /home/user/Documents </AnimatedSpan> </Terminal> );}
On branch main. Changes not staged for commit:
modified: packages/ui/src/terminal.tsx
[main 9b8c12a] feat: magic terminal
1 file changed, 24 insertions(+)
import { AnimatedSpan, Terminal, TypingAnimation,} from "@/components/sarahui/terminal"; export function TerminalMultiCommand() { return ( <Terminal> <TypingAnimation>$ git status</TypingAnimation> <AnimatedSpan className="text-blue-500">On branch main. Changes not staged for commit:</AnimatedSpan> <AnimatedSpan className="text-muted-foreground"> modified: packages/ui/src/terminal.tsx</AnimatedSpan> <TypingAnimation>$ git commit -m "feat: magic terminal"</TypingAnimation> <AnimatedSpan className="text-green-500">[main 9b8c12a] feat: magic terminal</AnimatedSpan> <AnimatedSpan className="text-green-500"> 1 file changed, 24 insertions(+)</AnimatedSpan> </Terminal> );}
Configuration parameters and properties for Terminal.
| Prop | Type | Default | Description |
|---|---|---|---|
command |
string | '' |
Primary terminal command line string to type or display |
steps |
string[] | [] |
Sequence of interactive terminal steps and output lines |
output |
string | '' |
Static preformatted command execution output |
sequence |
boolean | false |
Whether to animate terminal command steps sequentially |
className |
string | '' |
Optional CSS class for terminal window container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for terminal container |
children |
any | - |
Terminal content or child elements (e.g. AnimatedSpan) |
| Prop | Type | Default | Description |
|---|---|---|---|
delay |
number | 0 |
Animation entrance delay in milliseconds |
className |
string | '' |
Custom CSS class for output row styling |
children |
any | - |
Span output text or markup |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Terminal({ command: "npm install sarahui", output: "Added 1 package in 0.12s." }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;justify-content:center;width:100%;"> <div class="s-terminal" role="region" aria-label="Terminal" aria-live="polite" data-s-terminal data-sequence="true"><div class="s-terminal-header"><div class="s-terminal-dots"><span class="s-dot-red"></span><span class="s-dot-yellow"></span><span class="s-dot-green"></span></div></div><pre><code> <span class="s-typing-animation s-terminal-typing" data-s-typing="> pnpm dlx sarahui@latest init" data-text="> pnpm dlx sarahui@latest init" data-s-terminal-item="typing" data-duration="60"><span class="s-typing-text"></span><span class="s-typing-cursor" aria-hidden="true"></span></span> <div class="s-terminal-span s-terminal-span--success" data-s-terminal-item="span">✔ Preflight checks.</div> <div class="s-terminal-span s-terminal-span--success" data-s-terminal-item="span">✔ Verifying framework. Found Next.js.</div> <div class="s-terminal-span s-terminal-span--success" data-s-terminal-item="span">✔ Validating Tailwind CSS.</div> <div class="s-terminal-span s-terminal-span--success" data-s-terminal-item="span">✔ Validating import alias.</div> <div class="s-terminal-span s-terminal-span--success" data-s-terminal-item="span">✔ Writing components.json.</div> <div class="s-terminal-span s-terminal-span--success" data-s-terminal-item="span">✔ Checking registry.</div> <div class="s-terminal-span s-terminal-span--success" data-s-terminal-item="span">✔ Updating tailwind.config.ts</div> <div class="s-terminal-span s-terminal-span--success" data-s-terminal-item="span">✔ Updating app/globals.css</div> <div class="s-terminal-span s-terminal-span--success" data-s-terminal-item="span">✔ Installing dependencies.</div> <div class="s-terminal-span s-terminal-span--info" data-s-terminal-item="span"><span>ℹ Updated 1 file:</span><span style="padding-left:8px;">- lib/utils.ts</span></div> <span class="s-typing-animation s-terminal-typing is-muted" data-s-typing="Success! Project initialization completed." data-text="Success! Project initialization completed." data-s-terminal-item="typing" data-duration="60"><span class="s-typing-text"></span><span class="s-typing-cursor" aria-hidden="true"></span></span> <span class="s-typing-animation s-terminal-typing is-muted" data-s-typing="You may now add components." data-text="You may now add components." data-s-terminal-item="typing" data-duration="60"><span class="s-typing-text"></span><span class="s-typing-cursor" aria-hidden="true"></span></span> </code></pre></div> </div> Hero video thumbnail trigger with pulsing play button and accessible modal lightbox.
import { HeroVideoDialog } from "@/components/sarahui/hero-video-dialog"; export function HeroVideoDialogDemo() { return ( <div className="relative flex items-center justify-center p-6"> <HeroVideoDialog className="block" animationStyle="from-center" videoSrc="https://www.youtube.com/embed/qh3NGpYRG3I?si=4rb-zSdDkVK9qxxb" thumbnailSrc="https://images.unsplash.com/photo-1579546929518-9e396f3cc809?w=1200&auto=format&fit=crop&q=80" thumbnailAlt="SarahUI Component Architecture Overview" /> </div> );}
Add HeroVideoDialog to your project.
$ npx sarahui add hero-video-dialog
import { HeroVideoDialog } from 'sarahui';
<HeroVideoDialog className="block dark:hidden" animationStyle="from-center" videoSrc="https://www.example.com/dummy-video" thumbnailSrc="https://www.example.com/dummy-thumbnail.png" thumbnailAlt="Dummy Video Thumbnail"/>
import { HeroVideoDialog } from "@/components/sarahui/hero-video-dialog"; export function HeroVideoDialogTopIn() { return ( <HeroVideoDialog animationStyle="top-in-bottom-out" videoSrc="assets/video.html" thumbnailSrc="assets/photo-8.svg" thumbnailAlt="Platform Overview" /> );}
import { HeroVideoDialog } from "@/components/sarahui/hero-video-dialog"; export function HeroVideoDialogFromBottom() { return ( <HeroVideoDialog animationStyle="from-bottom" videoSrc="assets/video.html" thumbnailSrc="assets/photo-8.svg" thumbnailAlt="Platform Overview" /> );}
Configuration parameters and properties for HeroVideoDialog.
| Prop | Type | Default | Description |
|---|---|---|---|
videoSrc |
string | required |
URL of the video stream or embed player to show in modal dialog |
thumbnailSrc |
string | required |
URL of the video thumbnail poster preview image |
thumbnailAlt |
string | 'Video thumbnail' |
Accessible description for the thumbnail image |
animationStyle |
'from-center' | 'from-bottom' | 'from-top' | 'from-left' | 'from-right' | 'fade' | 'top-in-bottom-out' | 'left-in-right-out' | 'from-center' |
Modal dialog entrance and exit animation transition |
className |
string | '' |
Optional CSS class for dialog trigger container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for trigger element |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.HeroVideoDialog({ videoSrc: "https://www.youtube.com/embed/dQw4w9WgXcQ", thumbnailAlt: "Showcase" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="width:100%;max-width:480px;margin:0 auto;"><div class="s-hero-video-wrap"><button type="button" class="s-hero-video-thumbnail" data-s-video-open="s-videodialog-1" aria-haspopup="dialog" aria-label="Play video"><img src="assets/photo-8.svg" alt="sarahUI Showcase Walkthrough" class="s-hero-video-img"><div class="s-hero-video-play-backdrop"><div class="s-hero-video-play-halo"><div class="s-hero-video-play-btn"><svg class="s-hero-video-play-icon" viewBox="0 0 24 24" fill="currentColor"><path d="M8 5v14l11-7z"/></svg></div></div></div></button><dialog id="s-videodialog-1" class="s-dialog s-video-modal s-video-anim--from-center" data-video-src="assets/video.html"><div class="s-dialog-backdrop" data-s-dialog-close aria-hidden="true"></div><div class="s-video-modal-content"><button type="button" class="s-video-modal-close" data-s-dialog-close aria-label="Close video"><svg class="s-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button><div class="s-video-modal-frame"><iframe src="" title="Video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen></iframe></div></div></dialog></div></div>Responsive Bento-box grid layout with hover elevation and contextual cards.
import { BentoGrid, BentoCard } from "@/components/sarahui/bento-grid";import { FileTextIcon, BellIcon, Share2Icon, CalendarIcon } from "sarahui/icons"; const features = [ { Icon: FileTextIcon, name: "Save your files", description: "We automatically save your files as you type.", href: "#", cta: "Learn more", className: "col-span-3 lg:col-span-1", background: <div className="absolute right-0 top-0 origin-top rounded-md transition-all duration-300 ease-out [mask-image:linear-gradient(to_top,transparent_40%,#000_100%)] group-hover:scale-105" />, }, { Icon: BellIcon, name: "Notifications", description: "Get notified when something happens.", href: "#", cta: "Learn more", className: "col-span-3 lg:col-span-2", background: <div className="absolute right-0 top-0 origin-top rounded-md transition-all duration-300 ease-out [mask-image:linear-gradient(to_top,transparent_40%,#000_100%)] group-hover:scale-105" />, }, { Icon: Share2Icon, name: "Integrations", description: "Supports 100+ integrations and count.", href: "#", cta: "Learn more", className: "col-span-3 lg:col-span-2", background: <div className="absolute right-0 top-0 origin-top rounded-md transition-all duration-300 ease-out [mask-image:linear-gradient(to_top,transparent_40%,#000_100%)] group-hover:scale-105" />, }, { Icon: CalendarIcon, name: "Calendar", description: "Use the calendar to filter your files by date.", href: "#", cta: "Learn more", className: "col-span-3 lg:col-span-1", background: <div className="absolute right-0 top-0 origin-top rounded-md transition-all duration-300 ease-out [mask-image:linear-gradient(to_top,transparent_40%,#000_100%)] group-hover:scale-105" />, },]; export function BentoDemo() { return ( <BentoGrid> {features.map((feature, idx) => ( <BentoCard key={idx} {...feature} /> ))} </BentoGrid> );}
Add BentoGrid to your project.
$ npx sarahui add bento-grid
import { BentoGrid } from 'sarahui';
<BentoGrid> <BentoCard /></BentoGrid>
import { BentoCard, BentoGrid } from "@/components/sarahui/bento-grid"; export function BentoDemoVertical() { return ( <BentoGrid className="lg:grid-rows-3"> {features.map((feature) => ( <BentoCard key={feature.name} {...feature} /> ))} </BentoGrid> );}
import { BentoCard, BentoGrid } from "@/components/sarahui/bento-grid"; export function BentoGridThreeColumns() { return ( <BentoGrid className="grid-cols-3"> {features.map((feature) => ( <BentoCard key={feature.name} {...feature} /> ))} </BentoGrid> );}
Configuration parameters and properties for BentoGrid.
| Prop | Type | Default | Description |
|---|---|---|---|
columns |
number | 3 |
Number of columns for grid layout (e.g. 2, 3, 4) |
cols |
number | null | null |
Alternative column count configuration |
className |
string | '' |
Optional CSS class for grid container styling |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for grid element |
children |
any | - |
BentoCard components or card items to arrange in the grid |
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string | required |
Card title or headline |
title |
string | '' |
Alternative title property |
description |
string | required |
Feature description body text |
graphic |
any | - |
Visual header graphic or illustration |
background |
any | - |
Background illustration or graphic layer |
icon |
string | '' |
Icon name identifier |
Icon |
any | - |
Custom icon element or SVG |
cta |
string | 'Learn more' |
Call-to-action link button label |
href |
string | '#' |
Destination URL for card link and CTA |
colSpan |
number | 1 |
Number of grid columns spanned by card |
rowSpan |
number | 1 |
Number of grid rows spanned by card |
colStart |
number | - |
Grid column starting line index |
colEnd |
number | - |
Grid column ending line index |
rowStart |
number | - |
Grid row starting line index |
rowEnd |
number | - |
Grid row ending line index |
className |
string | '' |
Custom CSS class for card article container |
children |
any | - |
Additional card body content |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.BentoGrid({ columns: 2, children: UI.BentoCard({ title: "Tokens", description: "Design tokens system.", graphic: UI.Icon({ name: "palette" }) }) }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="width:100%;max-width:860px;"> <div class="s-bento-grid"> <article class="s-bento-card group s-bento-card--col-1"><div class="s-bento-card-bg"> <div style="position:absolute;top:16px;inset-inline:0;height:120px;mask-image:linear-gradient(to top,transparent 5%,#000 100%);-webkit-mask-image:linear-gradient(to top,transparent 5%,#000 100%);overflow:hidden;"> <div class="s-marquee group s-marquee--pause" style="--duration:20s;--gap:1rem;--s-gap:1rem;" data-s-marquee><div class="s-marquee-track"> <div style="display:flex;gap:8px;"> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">bitcoin.pdf</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Bitcoin cryptocurrency whitepaper by Satoshi Nakamoto.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">finances.xlsx</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Spreadsheet for calculating financial metrics and balance sheets.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">logo.svg</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Scalable Vector Graphics vector brand assets.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">keys.gpg</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Encrypted GPG key pairs for secure authentication.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">seed.txt</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Cryptographic recovery phrase backed up securely.</p> </figure> </div> </div><div class="s-marquee-track" aria-hidden="true"> <div style="display:flex;gap:8px;"> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">bitcoin.pdf</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Bitcoin cryptocurrency whitepaper by Satoshi Nakamoto.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">finances.xlsx</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Spreadsheet for calculating financial metrics and balance sheets.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">logo.svg</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Scalable Vector Graphics vector brand assets.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">keys.gpg</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Encrypted GPG key pairs for secure authentication.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">seed.txt</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Cryptographic recovery phrase backed up securely.</p> </figure> </div> </div><div class="s-marquee-track" aria-hidden="true"> <div style="display:flex;gap:8px;"> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">bitcoin.pdf</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Bitcoin cryptocurrency whitepaper by Satoshi Nakamoto.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">finances.xlsx</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Spreadsheet for calculating financial metrics and balance sheets.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">logo.svg</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Scalable Vector Graphics vector brand assets.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">keys.gpg</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Encrypted GPG key pairs for secure authentication.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">seed.txt</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Cryptographic recovery phrase backed up securely.</p> </figure> </div> </div><div class="s-marquee-track" aria-hidden="true"> <div style="display:flex;gap:8px;"> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">bitcoin.pdf</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Bitcoin cryptocurrency whitepaper by Satoshi Nakamoto.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">finances.xlsx</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Spreadsheet for calculating financial metrics and balance sheets.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">logo.svg</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Scalable Vector Graphics vector brand assets.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">keys.gpg</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Encrypted GPG key pairs for secure authentication.</p> </figure> <figure style="width:125px;padding:8px 10px;border-radius:10px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);font-size:11px;filter:blur(0.5px);transition:filter 0.2s;" onmouseenter="this.style.filter='none'" onmouseleave="this.style.filter='blur(0.5px)'"> <div style="display:flex;align-items:center;gap:4px;margin-bottom:2px;"> <strong style="font-size:11px;color:var(--s-text-primary);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">seed.txt</strong> </div> <p class="s-small s-muted" style="font-size:9.5px;line-height:1.3;margin:0;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;">Cryptographic recovery phrase backed up securely.</p> </figure> </div> </div></div> </div> </div><div class="s-bento-card-body s-bento-card-content"><div class="s-bento-card-icon"><svg class="s-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h7l2 3h9v12H3Zm0 0V3h7l2 3h9v3"/></svg></div><h3>Save your files</h3><p>We automatically save your files as you type.</p><div class="s-bento-card-cta"><a class="s-bento-btn" href="#components"><span>Learn more</span> <svg class="s-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"/></svg></a></div></div></article> <article class="s-bento-card group s-bento-card--col-2"><div class="s-bento-card-bg"> <div style="position:absolute;top:16px;right:16px;width:calc(100% - 32px);max-width:320px;height:180px;mask-image:linear-gradient(to top,transparent 5%,#000 100%);-webkit-mask-image:linear-gradient(to top,transparent 5%,#000 100%);overflow:hidden;"> <div class="s-animated-list" role="log" aria-live="polite" data-s-animated-list data-delay="1500" data-items="["<div class=\"s-anim-list-card\" style=\"padding:8px 12px;border-radius:10px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);display:flex;align-items:center;gap:10px;width:100%;\">\n <div style=\"width:28px;height:28px;border-radius:8px;background:color-mix(in srgb, #FFB800 15%, transparent);color:#FFB800;display:grid;place-items:center;font-size:13px;\">👤</div>\n <div style=\"flex:1;min-width:0;\"><div style=\"font-size:11.5px;font-weight:600;color:var(--s-text-primary);\">User signed up <span class=\"s-small s-muted\">· 10m ago</span></div><div class=\"s-small s-muted\" style=\"font-size:10px;\">sarahUI platform</div></div>\n </div>","<div class=\"s-anim-list-card\" style=\"padding:8px 12px;border-radius:10px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);display:flex;align-items:center;gap:10px;width:100%;\">\n <div style=\"width:28px;height:28px;border-radius:8px;background:color-mix(in srgb, var(--s-status-success-text) 15%, transparent);color:var(--s-status-success-text);display:grid;place-items:center;font-size:13px;\">💸</div>\n <div style=\"flex:1;min-width:0;\"><div style=\"font-size:11.5px;font-weight:600;color:var(--s-text-primary);\">Payment received <span class=\"s-small s-muted\">· 15m ago</span></div><div class=\"s-small s-muted\" style=\"font-size:10px;\">sarahUI store · $450.00</div></div>\n </div>","<div class=\"s-anim-list-card\" style=\"padding:8px 12px;border-radius:10px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);display:flex;align-items:center;gap:10px;width:100%;\">\n <div style=\"width:28px;height:28px;border-radius:8px;background:color-mix(in srgb, #FF3D71 15%, transparent);color:#FF3D71;display:grid;place-items:center;font-size:13px;\">💬</div>\n <div style=\"flex:1;min-width:0;\"><div style=\"font-size:11.5px;font-weight:600;color:var(--s-text-primary);\">New message <span class=\"s-small s-muted\">· 5m ago</span></div><div class=\"s-small s-muted\" style=\"font-size:10px;\">Can we update the theme token?</div></div>\n </div>","<div class=\"s-anim-list-card\" style=\"padding:8px 12px;border-radius:10px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);display:flex;align-items:center;gap:10px;width:100%;\">\n <div style=\"width:28px;height:28px;border-radius:8px;background:color-mix(in srgb, #1E86FF 15%, transparent);color:#1E86FF;display:grid;place-items:center;font-size:13px;\">🗞️</div>\n <div style=\"flex:1;min-width:0;\"><div style=\"font-size:11.5px;font-weight:600;color:var(--s-text-primary);\">New event <span class=\"s-small s-muted\">· 2m ago</span></div><div class=\"s-small s-muted\" style=\"font-size:10px;\">Community meetup scheduled</div></div>\n </div>"]"><div class="s-animated-list-item"><div class="s-anim-list-card" style="padding:8px 12px;border-radius:10px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);display:flex;align-items:center;gap:10px;width:100%;"> <div style="width:28px;height:28px;border-radius:8px;background:color-mix(in srgb, #FFB800 15%, transparent);color:#FFB800;display:grid;place-items:center;font-size:13px;">👤</div> <div style="flex:1;min-width:0;"><div style="font-size:11.5px;font-weight:600;color:var(--s-text-primary);">User signed up <span class="s-small s-muted">· 10m ago</span></div><div class="s-small s-muted" style="font-size:10px;">sarahUI platform</div></div> </div></div></div> </div> </div><div class="s-bento-card-body s-bento-card-content"><div class="s-bento-card-icon"><svg class="s-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 17h14l-2-3V9a5 5 0 0 0-10 0v5Zm5 3h4"/></svg></div><h3>Notifications</h3><p>Get notified when something happens.</p><div class="s-bento-card-cta"><a class="s-bento-btn" href="#components"><span>Learn more</span> <svg class="s-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"/></svg></a></div></div></article> <article class="s-bento-card group s-bento-card--col-2"><div class="s-bento-card-bg"> <div style="position:absolute;top:10px;inset-inline:0;height:140px;mask-image:linear-gradient(to top,transparent 5%,#000 100%);-webkit-mask-image:linear-gradient(to top,transparent 5%,#000 100%);overflow:visible;"> <svg viewBox="0 0 400 140" style="position:absolute;inset:0;width:100%;height:100%;" preserveAspectRatio="none"> <defs> <linearGradient id="s-bento-beam-1" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="400" y2="0"> <stop offset="0%" stop-color="#18CCFC" stop-opacity="0"></stop> <stop offset="50%" stop-color="#6344F5"></stop> <stop offset="100%" stop-color="#AE48FF" stop-opacity="0"></stop> </linearGradient> <linearGradient id="s-bento-beam-2" gradientUnits="userSpaceOnUse" x1="0" y1="0" x2="400" y2="0"> <stop offset="0%" stop-color="#10B981" stop-opacity="0"></stop> <stop offset="50%" stop-color="#3B82F6"></stop> <stop offset="100%" stop-color="#8B5CF6" stop-opacity="0"></stop> </linearGradient> </defs> <!-- Background static tracks --> <path class="s-beam-track" d="M 50 30 C 130 30, 140 70, 200 70"></path> <path class="s-beam-track" d="M 50 70 C 130 70, 140 70, 200 70"></path> <path class="s-beam-track" d="M 50 110 C 130 110, 140 70, 200 70"></path> <path class="s-beam-track" d="M 200 70 C 260 70, 270 45, 350 45"></path> <path class="s-beam-track" d="M 200 70 C 260 70, 270 95, 350 95"></path> <!-- Animated flowing beams --> <path class="s-anim-beam-line" stroke="url(#s-bento-beam-1)" stroke-width="2.5" stroke-linecap="round" d="M 50 30 C 130 30, 140 70, 200 70"></path> <path class="s-anim-beam-line--alt" stroke="url(#s-bento-beam-2)" stroke-width="2.5" stroke-linecap="round" d="M 50 70 C 130 70, 140 70, 200 70"></path> <path class="s-anim-beam-line" stroke="url(#s-bento-beam-1)" stroke-width="2.5" stroke-linecap="round" d="M 50 110 C 130 110, 140 70, 200 70"></path> <path class="s-anim-beam-line--alt" stroke="url(#s-bento-beam-2)" stroke-width="2.5" stroke-linecap="round" d="M 200 70 C 260 70, 270 45, 350 45"></path> <path class="s-anim-beam-line" stroke="url(#s-bento-beam-1)" stroke-width="2.5" stroke-linecap="round" d="M 200 70 C 260 70, 270 95, 350 95"></path> </svg> <!-- Connected circular nodes --> <div class="s-beam-node" style="left:12.5%;top:21%;">🌐</div> <div class="s-beam-node" style="left:12.5%;top:50%;">📁</div> <div class="s-beam-node" style="left:12.5%;top:79%;">💻</div> <div class="s-beam-node s-beam-node--center" style="left:50%;top:50%;"><svg class="s-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 2 2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5L2 12l7.5-2.5Z"/></svg></div> <div class="s-beam-node" style="left:87.5%;top:32%;">⚡</div> <div class="s-beam-node" style="left:87.5%;top:68%;">💬</div> </div> </div><div class="s-bento-card-body s-bento-card-content"><div class="s-bento-card-icon"><svg class="s-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 3 10 5-10 5L2 8Zm-9 9 9 5 9-5m-18 5 9 5 9-5"/></svg></div><h3>Integrations</h3><p>Supports 100+ integrations and counting.</p><div class="s-bento-card-cta"><a class="s-bento-btn" href="#components"><span>Learn more</span> <svg class="s-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"/></svg></a></div></div></article> <article class="s-bento-card group s-bento-card--col-1"><div class="s-bento-card-bg"> <div style="position:absolute;top:16px;right:16px;mask-image:linear-gradient(to top,transparent 5%,#000 100%);-webkit-mask-image:linear-gradient(to top,transparent 5%,#000 100%);"> <div style="padding:8px 12px;border-radius:10px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);font-size:10px;text-align:center;"> <strong style="display:block;font-size:11px;margin-bottom:4px;color:var(--s-text-primary);">September 2026</strong> <div style="display:grid;grid-template-columns:repeat(7,14px);gap:2px;font-size:8.5px;color:var(--s-text-muted);"> <span>Su</span><span>Mo</span><span>Tu</span><span>We</span><span>Th</span><span>Fr</span><span>Sa</span> <span style="opacity:0.3;">30</span><span style="opacity:0.3;">31</span><span style="font-weight:700;color:var(--s-text-brand);background:color-mix(in srgb,var(--s-brand-indigo) 20%, transparent);border-radius:50%;">1</span><span>2</span><span>3</span><span>4</span><span>5</span> </div> </div> </div> </div><div class="s-bento-card-body s-bento-card-content"><div class="s-bento-card-icon"><svg class="s-icon" width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 5h16v16H4ZM8 2v6m8-6v6M4 11h16"/></svg></div><h3>Calendar</h3><p>Use the calendar to filter your files by date.</p><div class="s-bento-card-cta"><a class="s-bento-btn" href="#components"><span>Learn more</span> <svg class="s-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"/></svg></a></div></div></article> </div> </div>Staggered animated notification stack with smooth entrance physics.
import { AnimatedList } from "@/components/sarahui/animated-list"; interface NotificationItem { name: string; description: string; icon: string; color: string; time: string;} const notifications: NotificationItem[] = [ { name: "Payment received", description: "sarahUI Enterprise", time: "15m ago", icon: "💸", color: "#00C9A7" }, { name: "User signed up", description: "sarahUI Studio", time: "10m ago", icon: "👤", color: "#FFB800" }, { name: "New message", description: "sarahUI Community", time: "5m ago", icon: "💬", color: "#FF3D71" }, { name: "New event", description: "sarahUI Release v0.1.0", time: "2m ago", icon: "🗞️", color: "#1E86FF" },]; export function AnimatedListDemo() { return ( <div className="relative flex h-[500px] w-full flex-col p-6 overflow-hidden rounded-lg border bg-background md:shadow-xl"> <AnimatedList delay={1500}> {notifications.map((item, idx) => ( <div key={idx} className="flex items-center gap-3 rounded-lg border p-4 shadow-sm bg-surface"> <span className="flex size-10 items-center justify-center rounded-full text-lg" style={{ backgroundColor: item.color }}> {item.icon} </span> <div className="flex flex-col overflow-hidden"> <span className="text-sm font-medium">{item.name}</span> <span className="text-xs text-muted">{item.description}</span> </div> <span className="ml-auto text-xs text-muted">{item.time}</span> </div> ))} </AnimatedList> </div> );}
Add AnimatedList to your project.
$ npx sarahui add animated-list
import { AnimatedList } from 'sarahui';
<AnimatedList> <p>Item 1</p> <p>Item 2</p> <p>Item 3</p></AnimatedList>
import { AnimatedList } from "@/components/sarahui/animated-list"; export function AnimatedListAuditFeed() { return ( <AnimatedList delay={1400}> {auditLogs.map((log) => ( <AuditRow key={log.id} log={log} /> ))} </AnimatedList> );}
import { AnimatedList } from "@/components/sarahui/animated-list"; export function AnimatedListTeamFeed() { return ( <AnimatedList delay={1300}> {teamActivity.map((activity) => ( <ActivityCard key={activity.id} {...activity} /> ))} </AnimatedList> );}
Configuration parameters and properties for AnimatedList.
| Prop | Type | Default | Description |
|---|---|---|---|
items |
any[] | [] |
Array of item data or strings to render in the animated list |
children |
any | - |
Child notification items to animate in sequence |
delay |
number | 1000 |
Stagger delay (in ms) between consecutive item appearances |
className |
string | '' |
Optional CSS class for feed list container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for list container |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.AnimatedList({ items: [UI.Badge({ label: "Payment received" }), UI.Badge({ label: "Deployment complete" })] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;display:flex;height:440px;width:100%;flex-direction:column;overflow:hidden;padding:16px;align-items:center;justify-content:flex-start;"> <div class="s-animated-list" role="log" aria-live="polite" data-s-animated-list data-delay="1200" data-items="["<figure style=\"position:relative;margin:0 auto;width:100%;max-width:400px;cursor:pointer;overflow:hidden;border-radius:16px;padding:14px 16px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);\"><div style=\"display:flex;flex-direction:row;align-items:center;gap:12px;\"><div style=\"display:flex;width:40px;height:40px;align-items:center;justify-content:center;border-radius:14px;background-color:#00C9A7;font-size:18px;\">💸</div><div style=\"display:flex;flex-direction:column;overflow:hidden;\"><figcaption style=\"display:flex;flex-direction:row;align-items:center;font-size:14px;font-weight:600;color:var(--s-text-primary);\"><span>Payment received</span><span style=\"margin:0 4px;color:var(--s-text-muted);\">·</span><span style=\"font-size:12px;font-weight:400;color:var(--s-text-muted);\">15m ago</span></figcaption><p style=\"font-size:13px;font-weight:400;color:var(--s-text-secondary);margin:2px 0 0;\">sarahUI Studio · $450.00</p></div></div></figure>","<figure style=\"position:relative;margin:0 auto;width:100%;max-width:400px;cursor:pointer;overflow:hidden;border-radius:16px;padding:14px 16px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);\"><div style=\"display:flex;flex-direction:row;align-items:center;gap:12px;\"><div style=\"display:flex;width:40px;height:40px;align-items:center;justify-content:center;border-radius:14px;background-color:#FFB800;font-size:18px;\">👤</div><div style=\"display:flex;flex-direction:column;overflow:hidden;\"><figcaption style=\"display:flex;flex-direction:row;align-items:center;font-size:14px;font-weight:600;color:var(--s-text-primary);\"><span>User signed up</span><span style=\"margin:0 4px;color:var(--s-text-muted);\">·</span><span style=\"font-size:12px;font-weight:400;color:var(--s-text-muted);\">10m ago</span></figcaption><p style=\"font-size:13px;font-weight:400;color:var(--s-text-secondary);margin:2px 0 0;\">Sarah Khan joined workspace</p></div></div></figure>","<figure style=\"position:relative;margin:0 auto;width:100%;max-width:400px;cursor:pointer;overflow:hidden;border-radius:16px;padding:14px 16px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);\"><div style=\"display:flex;flex-direction:row;align-items:center;gap:12px;\"><div style=\"display:flex;width:40px;height:40px;align-items:center;justify-content:center;border-radius:14px;background-color:#FF3D71;font-size:18px;\">💬</div><div style=\"display:flex;flex-direction:column;overflow:hidden;\"><figcaption style=\"display:flex;flex-direction:row;align-items:center;font-size:14px;font-weight:600;color:var(--s-text-primary);\"><span>New message</span><span style=\"margin:0 4px;color:var(--s-text-muted);\">·</span><span style=\"font-size:12px;font-weight:400;color:var(--s-text-muted);\">5m ago</span></figcaption><p style=\"font-size:13px;font-weight:400;color:var(--s-text-secondary);margin:2px 0 0;\">Can we update the theme token?</p></div></div></figure>","<figure style=\"position:relative;margin:0 auto;width:100%;max-width:400px;cursor:pointer;overflow:hidden;border-radius:16px;padding:14px 16px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);\"><div style=\"display:flex;flex-direction:row;align-items:center;gap:12px;\"><div style=\"display:flex;width:40px;height:40px;align-items:center;justify-content:center;border-radius:14px;background-color:#1E86FF;font-size:18px;\">🗞️</div><div style=\"display:flex;flex-direction:column;overflow:hidden;\"><figcaption style=\"display:flex;flex-direction:row;align-items:center;font-size:14px;font-weight:600;color:var(--s-text-primary);\"><span>New event</span><span style=\"margin:0 4px;color:var(--s-text-muted);\">·</span><span style=\"font-size:12px;font-weight:400;color:var(--s-text-muted);\">2m ago</span></figcaption><p style=\"font-size:13px;font-weight:400;color:var(--s-text-secondary);margin:2px 0 0;\">Community meetup scheduled</p></div></div></figure>"]"><div class="s-animated-list-item"><figure style="position:relative;margin:0 auto;width:100%;max-width:400px;cursor:pointer;overflow:hidden;border-radius:16px;padding:14px 16px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);"><div style="display:flex;flex-direction:row;align-items:center;gap:12px;"><div style="display:flex;width:40px;height:40px;align-items:center;justify-content:center;border-radius:14px;background-color:#00C9A7;font-size:18px;">💸</div><div style="display:flex;flex-direction:column;overflow:hidden;"><figcaption style="display:flex;flex-direction:row;align-items:center;font-size:14px;font-weight:600;color:var(--s-text-primary);"><span>Payment received</span><span style="margin:0 4px;color:var(--s-text-muted);">·</span><span style="font-size:12px;font-weight:400;color:var(--s-text-muted);">15m ago</span></figcaption><p style="font-size:13px;font-weight:400;color:var(--s-text-secondary);margin:2px 0 0;">sarahUI Studio · $450.00</p></div></div></figure></div></div> <div style="pointer-events:none;position:absolute;inset:auto 0 0 0;height:25%;background:linear-gradient(to top,var(--s-bg-canvas),transparent);"></div> </div> macOS styled magnifying dock container with smooth spring cursor physics.
import React from "react";import { CalendarIcon, HomeIcon, MailIcon, PencilIcon } from "sarahui/icons";import { Dock, DockIcon } from "@/components/sarahui/dock"; export function DockDemo() { return ( <div className="relative flex h-[240px] w-full items-center justify-center"> <Dock direction="bottom"> <DockIcon> <a href="#" aria-label="Home" className="flex size-10 items-center justify-center"> <HomeIcon className="size-5" /> </a> </DockIcon> <DockIcon> <a href="#" aria-label="Blog" className="flex size-10 items-center justify-center"> <PencilIcon className="size-5" /> </a> </DockIcon> <DockIcon> <a href="#" aria-label="Events" className="flex size-10 items-center justify-center"> <CalendarIcon className="size-5" /> </a> </DockIcon> <DockIcon> <a href="#" aria-label="Contact" className="flex size-10 items-center justify-center"> <MailIcon className="size-5" /> </a> </DockIcon> </Dock> </div> );}
Add Dock to your project.
$ npx sarahui add dock
import { Dock } from 'sarahui';
<Dock> <DockIcon> <Home /> </DockIcon> <DockIcon> <Settings /> </DockIcon> <DockIcon> <Search /> </DockIcon></Dock>
import { Dock, DockIcon, Separator } from "@/components/sarahui/dock"; export function DockDemo() { return ( <Dock> <DockIcon><HomeIcon /></DockIcon> <DockIcon><BlogIcon /></DockIcon> <Separator /> <DockIcon><GitHubIcon /></DockIcon> <DockIcon><LinkedInIcon /></DockIcon> <DockIcon><XIcon /></DockIcon> <DockIcon><MailIcon /></DockIcon> </Dock> );}
import { Dock, DockIcon } from "@/components/sarahui/dock"; export function DockDemo2() { return ( <Dock direction="middle"> <DockIcon><GitHubIcon /></DockIcon> <DockIcon><GoogleDriveIcon /></DockIcon> <DockIcon><NotionIcon /></DockIcon> <DockIcon><WhatsAppIcon /></DockIcon> </Dock> );}
import { Dock, DockIcon } from "@/components/sarahui/dock"; export function DockDemo3() { return ( <Dock iconMagnification={60} iconDistance={100}> <DockIcon className="bg-black/10 dark:bg-white/10 rounded-full"> <GitHubIcon /> </DockIcon> <DockIcon className="bg-black/10 dark:bg-white/10 rounded-full"> <GoogleDriveIcon /> </DockIcon> <DockIcon className="bg-black/10 dark:bg-white/10 rounded-full"> <NotionIcon /> </DockIcon> <DockIcon className="bg-black/10 dark:bg-white/10 rounded-full"> <WhatsAppIcon /> </DockIcon> </Dock> );}
Configuration parameters and properties for Dock.
| Prop | Type | Default | Description |
|---|---|---|---|
direction |
'bottom' | 'top' | 'vertical' | 'bottom' |
Dock orientation and edge placement |
iconSize |
number | 40 |
Base icon size in pixels when idle |
iconMagnification |
number | 60 |
Maximum magnified icon size in pixels on cursor hover |
iconDistance |
number | 140 |
Proximity distance in pixels across which magnification applies |
items |
DockItem[] | [] |
Array of dock action items with icons and labels |
children |
any | - |
Custom dock icons or child elements |
className |
string | '' |
Optional CSS class for dock toolbar container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for toolbar |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Dock({ items: [{ icon: "folder", label: "Projects" }, { icon: "spark", label: "Effects" }, { icon: "settings", label: "Settings" }] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="dock-demo-stage" style="position:relative;width:100%;min-height:220px;display:flex;align-items:center;justify-content:center;border-radius:var(--s-radius-lg,16px);background:var(--s-bg-surface);border:var(--s-border-width-default,1px) solid var(--s-border-default);overflow:hidden;padding:var(--s-spacing-32) var(--s-spacing-16);"> <div style="position:relative;z-index:var(--s-layer-raised, 2);"> <nav class="s-dock" role="toolbar" data-s-dock data-size="40" data-magnification="60" data-distance="140" aria-label="Quick tools"><button type="button" class="s-dock-icon" title="Home" aria-label="Home"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"><path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><polyline points="9 22 9 12 15 12 15 22" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></button><button type="button" class="s-dock-icon" title="Blog" aria-label="Blog"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><path d="m15 5 4 4" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></button><div class="s-dock-separator" aria-hidden="true"></div><button type="button" class="s-dock-icon" title="GitHub" aria-label="GitHub"><svg class="s-icon" width="20" height="20" viewBox="0 0 438.549 438.549" aria-hidden="true"><path fill="currentColor" d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 0 1-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"/></svg></button><button type="button" class="s-dock-icon" title="LinkedIn" aria-label="LinkedIn"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg></button><button type="button" class="s-dock-icon" title="X" aria-label="X"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M18.901 1.153h3.68l-8.04 9.19L24 22.846h-7.406l-5.8-7.584-6.638 7.584H.474l8.6-9.83L0 1.154h7.594l5.243 6.932ZM17.61 20.644h2.039L6.486 3.24H4.298Z"/></svg></button><button type="button" class="s-dock-icon" title="Send Email" aria-label="Send Email"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"><rect width="20" height="16" x="2" y="4" rx="2" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></button></nav> </div> </div> Interactive canvas-rendered 3D wireframe vector spinning globe.
import { Globe } from "@/components/sarahui/globe"; export function GlobeDemo() { return ( <div className="relative flex h-[440px] w-full max-w-xl items-center justify-center overflow-hidden rounded-lg border bg-background pt-8 pb-40"> <span className="pointer-events-none bg-gradient-to-b from-black to-gray-300/80 bg-clip-text text-center text-8xl font-semibold leading-none text-transparent dark:from-white dark:to-slate-900/10"> Globe </span> <Globe className="top-28" /> </div> );}
Add Globe to your project.
$ npx sarahui add globe
import { Globe } from 'sarahui';
<Globe />
import { Globe } from "@/components/sarahui/globe"; export function GlobeInteractive() { return ( <div className="relative flex h-[440px] w-full max-w-xl items-center justify-center overflow-hidden rounded-xl border bg-background pt-8 pb-40"> <span className="pointer-events-none bg-gradient-to-b from-black to-gray-300/80 bg-clip-text text-center text-8xl font-semibold leading-none text-transparent dark:from-white dark:to-slate-900/10"> Globe </span> <Globe className="top-28" /> <div className="pointer-events-none absolute inset-0 h-full bg-[radial-gradient(circle_at_50%_200%,rgba(0,0,0,0.2),rgba(255,255,255,0))]" /> </div> );}
import { Globe } from "@/components/sarahui/globe"; export function GlobeCompact() { return ( <div className="relative flex h-[380px] w-full max-w-lg items-center justify-center overflow-hidden rounded-xl border bg-background pt-6 pb-32"> <span className="pointer-events-none bg-gradient-to-b from-black to-gray-300/80 bg-clip-text text-center text-6xl font-semibold leading-none text-transparent dark:from-white dark:to-slate-900/10"> World </span> <Globe className="top-24" /> <div className="pointer-events-none absolute inset-0 h-full bg-[radial-gradient(circle_at_50%_200%,rgba(0,0,0,0.2),rgba(255,255,255,0))]" /> </div> );}
Configuration parameters and properties for Globe.
| Prop | Type | Default | Description |
|---|---|---|---|
size |
number | string | 360 |
Diameter size of the interactive globe in pixels |
config |
Record<string, any> | {} |
Configuration parameters for globe rendering (rotation, markers) |
className |
string | '' |
Optional CSS class for canvas wrapper container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for canvas element |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Globe({ size: 280 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="globe-demo-stage" style="position:relative;display:flex;height:440px;width:100%;max-width:620px;flex-direction:column;align-items:center;justify-content:flex-start;overflow:hidden;border-radius:16px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);margin:0 auto;padding-top:36px;"> <span style="pointer-events:none;z-index:10;background:linear-gradient(180deg, var(--s-text-primary) 0%, color-mix(in srgb, var(--s-text-muted) 35%, transparent) 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-align:center;font-size:clamp(4.5rem, 11vw, 6rem);font-weight:700;line-height:1;letter-spacing:-0.04em;user-select:none;"> Globe </span> <div style="position:absolute;top:160px;left:50%;transform:translateX(-50%);width:640px;height:640px;max-width:none;display:flex;align-items:center;justify-content:center;z-index:2;"> <div class="s-globe-container" style="width:600px;height:600px;"><canvas class="s-globe-canvas" data-s-globe width="1200" height="1200"></canvas><div class="s-globe-glow"></div></div> </div> <div class="globe-stage-shadow"></div> </div> High-fidelity social post card with engagement stats and verified badge.
import { TweetCard } from "@/components/sarahui/tweet-card"; export function TweetCardDemo() { return ( <div className="flex items-center justify-center p-4"> <TweetCard id="2095541863956742174" className="shadow-2xl" /> </div> );}
Add TweetCard to your project.
$ npx sarahui add tweet-card
import { TweetCard } from 'sarahui';
"use client" import { TweetCard } from 'sarahui' export default function App() { return <TweetCard id="1441032681968212480" />}
import { TweetCard } from "sarahui"; export function TweetCardImagesDemo() { return ( <TweetCard name="Sarah Khan" handle="sarahui" avatar="/assets/sarah-mark.svg" images={[ "/assets/sample-1.svg", "/assets/sample-2.svg", "/assets/sample-3.svg", ]} className="shadow-2xl" /> );}
An original design system and component engineering library.
import { TweetCard } from "sarahui"; export function TweetCardMetaDemo() { return ( <TweetCard name="Sarah Khan" handle="sarahui" avatar="/assets/sarah-mark.svg" urlPreview={{ domain: "github.com", title: "masarax/sarahui", description: "An original design system and component engineering library.", url: "https://github.com/masarax/sarahui", }} className="shadow-2xl" /> );}
Configuration parameters and properties for TweetCard.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | '2095541863956742174' |
The unique ID of the tweet to render |
name |
string | '' |
Author display name |
authorName |
string | '' |
Alternative author name property |
handle |
string | '' |
Author handle or username |
avatar |
string | '' |
Author avatar image URL |
verified |
boolean | false |
Whether author displays verified badge |
text |
string | '' |
Tweet body text content |
content |
string | '' |
Alternative content text property |
timestamp |
string | '' |
Publication timestamp string |
date |
string | '' |
Alternative date string property |
likes |
number | string | 0 |
Like count displayed in tweet metrics |
retweets |
number | string | 0 |
Retweet / repost count displayed in tweet metrics |
video |
boolean | false |
Whether the tweet includes embedded video |
videoUrl |
string | '' |
Direct URL to video stream or file |
mediaUrl |
string | '' |
URL of primary embedded media image |
media |
any[] | [] |
Array of embedded media items |
images |
string[] | [] |
Array of image URLs for multi-photo grid |
urlPreview |
any | null |
Open Graph link preview card metadata |
autoPlay |
boolean | true |
Whether embedded video starts playing automatically |
muted |
boolean | true |
Whether embedded video starts muted |
loop |
boolean | true |
Whether embedded video loops continuously |
controls |
boolean | true |
Whether video playback controls are accessible |
showFooter |
boolean | true |
Whether to render tweet engagement metrics footer |
apiUrl |
string | '' |
Optional custom API proxy URL for fetching tweet JSON |
className |
string | '' |
Optional CSS class for card article container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for article element |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.TweetCard({ name: "Sarah Khan", handle: "sarahkhan", content: "Building with sarahUI!", verified: true }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="max-width:480px;width:100%;margin:0 auto;"> <article class="s-tweet-card" data-tweet-id="2095541863956742174"><header class="s-tweet-header"><div class="s-tweet-author"><a href="https://x.com/sarahui" target="_blank" rel="noopener noreferrer" class="s-tweet-avatar-link"><img src="assets/sarah-mark.svg" alt="Sarah Khan" class="s-tweet-avatar" loading="lazy" /></a><div class="s-tweet-author-info"><div class="s-tweet-name-row"><a href="https://x.com/sarahui" target="_blank" rel="noopener noreferrer" class="s-tweet-name-link"><strong class="s-tweet-name">Sarah Khan</strong></a><svg class="s-tweet-verified" width="16" height="16" viewBox="0 0 24 24" fill="#1D9BF0" aria-label="Verified"><path d="M22.5 12.5c0-1.58-.875-2.95-2.148-3.6.154-.435.238-.905.238-1.4 0-2.21-1.71-3.998-3.818-3.998-.47 0-.92.084-1.336.25C14.818 2.415 13.51 1.5 12 1.5s-2.816.917-3.437 2.25c-.415-.165-.866-.25-1.336-.25-2.11 0-3.818 1.79-3.818 4 0 .494.083.964.237 1.4-1.272.65-2.147 2.018-2.147 3.6 0 1.495.782 2.798 1.942 3.486-.02.17-.032.34-.032.514 0 2.21 1.708 4 3.818 4 .47 0 .92-.086 1.335-.25.62 1.334 1.926 2.25 3.437 2.25 1.512 0 2.818-.916 3.437-2.25.415.163.865.248 1.336.248 2.11 0 3.818-1.79 3.818-4 0-.174-.012-.344-.033-.513 1.158-.687 1.943-1.99 1.943-3.484zm-6.616-3.334l-4.334 6.5c-.145.217-.382.334-.625.334-.143 0-.288-.04-.416-.126l-.115-.094-2.415-2.415c-.293-.293-.293-.768 0-1.06s.768-.294 1.06 0l1.77 1.767 3.825-5.74c.23-.345.696-.436 1.04-.207.346.23.44.696.21 1.04z"/></svg></div><a href="https://x.com/sarahui" target="_blank" rel="noopener noreferrer" class="s-tweet-handle-link"><span class="s-tweet-handle">@sarahui</span></a></div></div><a href="https://x.com/sarahui/status/2095541863956742174" target="_blank" rel="noopener noreferrer" class="s-tweet-logo-link" aria-label="View on X"><svg class="s-tweet-logo" width="18" height="18" viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg></a></header><div class="s-tweet-body"><div class="s-tweet-content">Design systems should feel invisible: every state, color, and motion has one clear job.<br /><br />sarahUI ships 120 components and 319 tokens with zero runtime dependencies.</div></div></article> </div> Concentric orbital tracks rotating nested elements in 3D perspective.
import { OrbitingCircles } from "@/components/sarahui/orbiting-circles";import { FileIcon, SettingsIcon, SearchIcon } from "sarahui/icons"; export function OrbitingCirclesDemo() { return ( <div className="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background md:shadow-xl"> <span className="pointer-events-none whitespace-pre-wrap bg-gradient-to-b from-black to-gray-300 bg-clip-text text-center text-8xl font-semibold leading-none text-transparent dark:from-white dark:to-slate-900/10"> Circles </span> <OrbitingCircles className="size-[30px] border-none bg-transparent" duration={20} delay={20} radius={80}> <FileIcon className="size-6 text-brand" /> </OrbitingCircles> <OrbitingCircles className="size-[30px] border-none bg-transparent" duration={20} delay={10} radius={80}> <SettingsIcon className="size-6 text-brand" /> </OrbitingCircles> <OrbitingCircles className="size-[50px] border-none bg-transparent" radius={190} duration={20} reverse> <SearchIcon className="size-8 text-indigo-500" /> </OrbitingCircles> </div> );}
Add OrbitingCircles to your project.
$ npx sarahui add orbiting-circles
import { OrbitingCircles } from 'sarahui';
<div className="relative h-[500px] w-full overflow-hidden"> <OrbitingCircles> <File /> <Settings /> <File /> </OrbitingCircles> <OrbitingCircles radius={100} reverse> <File /> <Settings /> <File /> <Search /> </OrbitingCircles></div>
import { OrbitingCircles } from "@/components/sarahui/orbiting-circles";import { Icons } from "@/components/icons"; export function OrbitingCirclesDemo() { return ( <div className="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden"> <OrbitingCircles iconSize={40}> <Icons.whatsapp /> <Icons.notion /> <Icons.openai /> <Icons.googleDrive /> <Icons.gitHub /> </OrbitingCircles> <OrbitingCircles iconSize={30} radius={100} reverse speed={2}> <Icons.whatsapp /> <Icons.notion /> <Icons.openai /> <Icons.googleDrive /> </OrbitingCircles> </div> );}
import { OrbitingCircles } from "@/components/sarahui/orbiting-circles";import { Icons } from "@/components/icons"; export function OrbitingCirclesSingle() { return ( <div className="relative flex h-[400px] w-full flex-col items-center justify-center overflow-hidden"> <OrbitingCircles iconSize={40} radius={120}> <Icons.whatsapp /> <Icons.notion /> <Icons.openai /> <Icons.googleDrive /> </OrbitingCircles> </div> );}
Configuration parameters and properties for OrbitingCircles.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
any | - |
Icon elements to place along orbital track |
items |
any[] | [] |
Array of icon data or objects to orbit along path |
icons |
any[] | [] |
Alternative icon objects property |
radius |
number | 50 |
Radius of the orbital circle path in pixels |
duration |
number | 20 |
Time in seconds for one complete 360-degree orbit |
delay |
number | 10 |
Initial animation delay in seconds |
reverse |
boolean | false |
Whether orbital motion moves counter-clockwise |
path |
boolean | true |
Whether to render the subtle circular background orbit track |
iconSize |
number | 30 |
Size of orbiting icon nodes in pixels |
speed |
number | 1 |
Animation speed multiplier |
className |
string | '' |
Optional CSS class for orbit container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for container |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.OrbitingCircles({ radius: 80, icons: ["whatsapp", "notion", "openai", "drive", "github"] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;display:flex;height:500px;width:100%;flex-direction:column;align-items:center;justify-content:center;overflow:hidden;"> <div class="s-orbiting-circles"><svg class="s-orbit-circle-path" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"><circle cx="50%" cy="50%" r="160" fill="none"/></svg><div class="s-orbit-item" style="--radius:160;--duration:20;--delay:10;--angle:0;--icon-size:40px;"><svg class="s-icon" width="40" height="40" viewBox="0 0 175.216 175.552" aria-hidden="true"><g><path d="m12.966 161.238 10.439-38.114a73.42 73.42 0 0 1-9.821-36.772c.017-40.556 33.021-73.55 73.578-73.55 19.681.01 38.154 7.669 52.047 21.572s21.537 32.383 21.53 52.037c-.018 40.553-33.027 73.553-73.578 73.553h-.032c-12.313-.005-24.412-3.094-35.159-8.954z" fill="#25D366"/><path d="M87.184 25.227c-33.733 0-61.166 27.423-61.178 61.13a60.98 60.98 0 0 0 9.349 32.535l1.455 2.312-6.179 22.559 23.146-6.069 2.235 1.324c9.387 5.571 20.15 8.518 31.126 8.524h.023c33.707 0 61.14-27.426 61.153-61.135a60.75 60.75 0 0 0-17.895-43.251 60.75 60.75 0 0 0-43.235-17.929z" fill="#25D366"/><path d="M68.772 55.603c-1.378-3.061-2.828-3.123-4.137-3.176l-3.524-.043c-1.226 0-3.218.46-4.902 2.3s-6.435 6.287-6.435 15.332 6.588 17.785 7.506 19.013 12.718 20.381 31.405 27.75c15.529 6.124 18.689 4.906 22.061 4.6s10.877-4.447 12.408-8.74 1.532-7.971 1.073-8.74-1.685-1.226-3.525-2.146-10.877-5.367-12.562-5.981-2.91-.919-4.137.921-4.746 5.979-5.819 7.206-2.144 1.381-3.984.462-7.76-2.861-14.784-9.124c-5.465-4.873-9.154-10.891-10.228-12.73s-.114-2.835.808-3.751c.825-.824 1.838-2.147 2.759-3.22s1.224-1.84 1.836-3.065.307-2.301-.153-3.22-4.032-10.011-5.666-13.647" fill="#ffffff" fill-rule="evenodd"/></g></svg></div><div class="s-orbit-item" style="--radius:160;--duration:20;--delay:10;--angle:72;--icon-size:40px;"><svg class="s-icon" width="40" height="40" viewBox="0 0 100 100" aria-hidden="true"><g><path d="M6.017 4.313l55.333 -4.087c6.797 -0.583 8.543 -0.19 12.817 2.917l17.663 12.443c2.913 2.14 3.883 2.723 3.883 5.053v68.243c0 4.277 -1.553 6.807 -6.99 7.193L24.467 99.967c-4.08 0.193 -6.023 -0.39 -8.16 -3.113L3.3 79.94c-2.333 -3.113 -3.3 -5.443 -3.3 -8.167V11.113c0 -3.497 1.553 -6.413 6.017 -6.8z" fill="#ffffff"/><path d="M61.35 0.227l-55.333 4.087C1.553 4.7 0 7.617 0 11.113v60.66c0 2.723 0.967 5.053 3.3 8.167l13.007 16.913c2.137 2.723 4.08 3.307 8.16 3.113l64.257 -3.89c5.433 -0.387 6.99 -2.917 6.99 -7.193V20.64c0 -2.21 -0.873 -2.847 -3.443 -4.733L74.167 3.143c-4.273 -3.107 -6.02 -3.5 -12.817 -2.917zM25.92 19.523c-5.247 0.353 -6.437 0.433 -9.417 -1.99L8.927 11.507c-0.77 -0.78 -0.383 -1.753 1.557 -1.947l53.193 -3.887c4.467 -0.39 6.793 1.167 8.54 2.527l9.123 6.61c0.39 0.197 1.36 1.36 0.193 1.36l-54.933 3.307 -0.68 0.047zM19.803 88.3V30.367c0 -2.53 0.777 -3.697 3.103 -3.893L86 22.78c2.14 -0.193 3.107 1.167 3.107 3.693v57.547c0 2.53 -0.39 4.67 -3.883 4.863l-60.377 3.5c-3.493 0.193 -5.043 -0.97 -5.043 -4.083zm59.6 -54.827c0.387 1.75 0 3.5 -1.75 3.7l-2.91 0.577v42.773c-2.527 1.36 -4.853 2.137 -6.797 2.137 -3.107 0 -3.883 -0.973 -6.21 -3.887l-19.03 -29.94v28.967l6.02 1.363s0 3.5 -4.857 3.5l-13.39 0.777c-0.39 -0.78 0 -2.723 1.357 -3.11l3.497 -0.97v-38.3L30.48 40.667c-0.39 -1.75 0.58 -4.277 3.3 -4.473l14.367 -0.967 19.8 30.327v-26.83l-5.047 -0.58c-0.39 -2.143 1.163 -3.7 3.103 -3.89l13.4 -0.78z" fill="#000000" fill-rule="evenodd" clip-rule="evenodd"/></g></svg></div><div class="s-orbit-item" style="--radius:160;--duration:20;--delay:10;--angle:144;--icon-size:40px;"><svg class="s-icon" width="40" height="40" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg></div><div class="s-orbit-item" style="--radius:160;--duration:20;--delay:10;--angle:216;--icon-size:40px;"><svg class="s-icon" width="40" height="40" viewBox="0 0 87.3 78" aria-hidden="true"><g><path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da"/><path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47"/><path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#ea4335"/><path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d"/><path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc"/><path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#ffba00"/></g></svg></div><div class="s-orbit-item" style="--radius:160;--duration:20;--delay:10;--angle:288;--icon-size:40px;"><svg class="s-icon" width="40" height="40" viewBox="0 0 438.549 438.549" aria-hidden="true"><path fill="currentColor" d="M409.132 114.573c-19.608-33.596-46.205-60.194-79.798-79.8-33.598-19.607-70.277-29.408-110.063-29.408-39.781 0-76.472 9.804-110.063 29.408-33.596 19.605-60.192 46.204-79.8 79.8C9.803 148.168 0 184.854 0 224.63c0 47.78 13.94 90.745 41.827 128.906 27.884 38.164 63.906 64.572 108.063 79.227 5.14.954 8.945.283 11.419-1.996 2.475-2.282 3.711-5.14 3.711-8.562 0-.571-.049-5.708-.144-15.417a2549.81 2549.81 0 0 1-.144-25.406l-6.567 1.136c-4.187.767-9.469 1.092-15.846 1-6.374-.089-12.991-.757-19.842-1.999-6.854-1.231-13.229-4.086-19.13-8.559-5.898-4.473-10.085-10.328-12.56-17.556l-2.855-6.57c-1.903-4.374-4.899-9.233-8.992-14.559-4.093-5.331-8.232-8.945-12.419-10.848l-1.999-1.431c-1.332-.951-2.568-2.098-3.711-3.429-1.142-1.331-1.997-2.663-2.568-3.997-.572-1.335-.098-2.43 1.427-3.289 1.525-.859 4.281-1.276 8.28-1.276l5.708.853c3.807.763 8.516 3.042 14.133 6.851 5.614 3.806 10.229 8.754 13.846 14.842 4.38 7.806 9.657 13.754 15.846 17.847 6.184 4.093 12.419 6.136 18.699 6.136 6.28 0 11.704-.476 16.274-1.423 4.565-.952 8.848-2.383 12.847-4.285 1.713-12.758 6.377-22.559 13.988-29.41-10.848-1.14-20.601-2.857-29.264-5.14-8.658-2.286-17.605-5.996-26.835-11.14-9.235-5.137-16.896-11.516-22.985-19.126-6.09-7.614-11.088-17.61-14.987-29.979-3.901-12.374-5.852-26.648-5.852-42.826 0-23.035 7.52-42.637 22.557-58.817-7.044-17.318-6.379-36.732 1.997-58.24 5.52-1.715 13.706-.428 24.554 3.853 10.85 4.283 18.794 7.952 23.84 10.994 5.046 3.041 9.089 5.618 12.135 7.708 17.705-4.947 35.976-7.421 54.818-7.421s37.117 2.474 54.823 7.421l10.849-6.849c7.419-4.57 16.18-8.758 26.262-12.565 10.088-3.805 17.802-4.853 23.134-3.138 8.562 21.509 9.325 40.922 2.279 58.24 15.036 16.18 22.559 35.787 22.559 58.817 0 16.178-1.958 30.497-5.853 42.966-3.9 12.471-8.941 22.457-15.125 29.979-6.191 7.521-13.901 13.85-23.131 18.986-9.232 5.14-18.182 8.85-26.84 11.136-8.662 2.286-18.415 4.004-29.263 5.146 9.894 8.562 14.842 22.077 14.842 40.539v60.237c0 3.422 1.19 6.279 3.572 8.562 2.379 2.279 6.136 2.95 11.276 1.995 44.163-14.653 80.185-41.062 108.068-79.226 27.88-38.161 41.825-81.126 41.825-128.906-.01-39.771-9.818-76.454-29.414-110.049z"/></svg></div></div> <div class="s-orbiting-circles s-orbit-reverse"><svg class="s-orbit-circle-path" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%"><circle cx="50%" cy="50%" r="100" fill="none"/></svg><div class="s-orbit-item s-orbit-reverse" style="--radius:100;--duration:10;--delay:10;--angle:0;--icon-size:30px;"><svg class="s-icon" width="30" height="30" viewBox="0 0 175.216 175.552" aria-hidden="true"><g><path d="m12.966 161.238 10.439-38.114a73.42 73.42 0 0 1-9.821-36.772c.017-40.556 33.021-73.55 73.578-73.55 19.681.01 38.154 7.669 52.047 21.572s21.537 32.383 21.53 52.037c-.018 40.553-33.027 73.553-73.578 73.553h-.032c-12.313-.005-24.412-3.094-35.159-8.954z" fill="#25D366"/><path d="M87.184 25.227c-33.733 0-61.166 27.423-61.178 61.13a60.98 60.98 0 0 0 9.349 32.535l1.455 2.312-6.179 22.559 23.146-6.069 2.235 1.324c9.387 5.571 20.15 8.518 31.126 8.524h.023c33.707 0 61.14-27.426 61.153-61.135a60.75 60.75 0 0 0-17.895-43.251 60.75 60.75 0 0 0-43.235-17.929z" fill="#25D366"/><path d="M68.772 55.603c-1.378-3.061-2.828-3.123-4.137-3.176l-3.524-.043c-1.226 0-3.218.46-4.902 2.3s-6.435 6.287-6.435 15.332 6.588 17.785 7.506 19.013 12.718 20.381 31.405 27.75c15.529 6.124 18.689 4.906 22.061 4.6s10.877-4.447 12.408-8.74 1.532-7.971 1.073-8.74-1.685-1.226-3.525-2.146-10.877-5.367-12.562-5.981-2.91-.919-4.137.921-4.746 5.979-5.819 7.206-2.144 1.381-3.984.462-7.76-2.861-14.784-9.124c-5.465-4.873-9.154-10.891-10.228-12.73s-.114-2.835.808-3.751c.825-.824 1.838-2.147 2.759-3.22s1.224-1.84 1.836-3.065.307-2.301-.153-3.22-4.032-10.011-5.666-13.647" fill="#ffffff" fill-rule="evenodd"/></g></svg></div><div class="s-orbit-item s-orbit-reverse" style="--radius:100;--duration:10;--delay:10;--angle:90;--icon-size:30px;"><svg class="s-icon" width="30" height="30" viewBox="0 0 100 100" aria-hidden="true"><g><path d="M6.017 4.313l55.333 -4.087c6.797 -0.583 8.543 -0.19 12.817 2.917l17.663 12.443c2.913 2.14 3.883 2.723 3.883 5.053v68.243c0 4.277 -1.553 6.807 -6.99 7.193L24.467 99.967c-4.08 0.193 -6.023 -0.39 -8.16 -3.113L3.3 79.94c-2.333 -3.113 -3.3 -5.443 -3.3 -8.167V11.113c0 -3.497 1.553 -6.413 6.017 -6.8z" fill="#ffffff"/><path d="M61.35 0.227l-55.333 4.087C1.553 4.7 0 7.617 0 11.113v60.66c0 2.723 0.967 5.053 3.3 8.167l13.007 16.913c2.137 2.723 4.08 3.307 8.16 3.113l64.257 -3.89c5.433 -0.387 6.99 -2.917 6.99 -7.193V20.64c0 -2.21 -0.873 -2.847 -3.443 -4.733L74.167 3.143c-4.273 -3.107 -6.02 -3.5 -12.817 -2.917zM25.92 19.523c-5.247 0.353 -6.437 0.433 -9.417 -1.99L8.927 11.507c-0.77 -0.78 -0.383 -1.753 1.557 -1.947l53.193 -3.887c4.467 -0.39 6.793 1.167 8.54 2.527l9.123 6.61c0.39 0.197 1.36 1.36 0.193 1.36l-54.933 3.307 -0.68 0.047zM19.803 88.3V30.367c0 -2.53 0.777 -3.697 3.103 -3.893L86 22.78c2.14 -0.193 3.107 1.167 3.107 3.693v57.547c0 2.53 -0.39 4.67 -3.883 4.863l-60.377 3.5c-3.493 0.193 -5.043 -0.97 -5.043 -4.083zm59.6 -54.827c0.387 1.75 0 3.5 -1.75 3.7l-2.91 0.577v42.773c-2.527 1.36 -4.853 2.137 -6.797 2.137 -3.107 0 -3.883 -0.973 -6.21 -3.887l-19.03 -29.94v28.967l6.02 1.363s0 3.5 -4.857 3.5l-13.39 0.777c-0.39 -0.78 0 -2.723 1.357 -3.11l3.497 -0.97v-38.3L30.48 40.667c-0.39 -1.75 0.58 -4.277 3.3 -4.473l14.367 -0.967 19.8 30.327v-26.83l-5.047 -0.58c-0.39 -2.143 1.163 -3.7 3.103 -3.89l13.4 -0.78z" fill="#000000" fill-rule="evenodd" clip-rule="evenodd"/></g></svg></div><div class="s-orbit-item s-orbit-reverse" style="--radius:100;--duration:10;--delay:10;--angle:180;--icon-size:30px;"><svg class="s-icon" width="30" height="30" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg></div><div class="s-orbit-item s-orbit-reverse" style="--radius:100;--duration:10;--delay:10;--angle:270;--icon-size:30px;"><svg class="s-icon" width="30" height="30" viewBox="0 0 87.3 78" aria-hidden="true"><g><path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da"/><path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47"/><path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#ea4335"/><path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d"/><path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc"/><path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#ffba00"/></g></svg></div></div> </div> Overlapping avatar cluster displaying active members and excess counts.
import { AvatarCircles } from "@/components/sarahui/avatar-circles"; const avatarUrls = [ "https://images.unsplash.com/photo-1534528741775-53994a69daeb?w=100&auto=format&fit=crop&q=80", "https://images.unsplash.com/photo-1517841905240-472988babdf9?w=100&auto=format&fit=crop&q=80", "https://images.unsplash.com/photo-1539571696357-5a69c17a67c6?w=100&auto=format&fit=crop&q=80", "https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=100&auto=format&fit=crop&q=80",]; export function AvatarCirclesDemo() { return <AvatarCircles numPeople={99} avatarUrls={avatarUrls} />;}
Add AvatarCircles to your project.
$ npx sarahui add avatar-circles
import { AvatarCircles } from 'sarahui';
<AvatarCircles numPeople={99} avatarUrls={[ { imageUrl: "https://avatars.githubusercontent.com/u/16860528", profileUrl: "https://github.com/sarahui", }, ]}/>
import { AvatarCircles } from "@/components/sarahui/avatar-circles"; const avatarUrls = [ "assets/avatar-1.svg", "assets/avatar-2.svg", "assets/avatar-3.svg", "assets/avatar-4.svg",]; export function AvatarCirclesDemo() { return <AvatarCircles numPeople={99} avatarUrls={avatarUrls} />;}
import { AvatarCircles } from "@/components/sarahui/avatar-circles"; const avatarUrls = [ "assets/avatar-1.svg", "assets/avatar-2.svg", "assets/avatar-3.svg",]; export function AvatarCirclesSmall() { return <AvatarCircles numPeople={42} avatarUrls={avatarUrls} />;}
Configuration parameters and properties for AvatarCircles.
| Prop | Type | Default | Description |
|---|---|---|---|
numPeople |
number | 99 |
Count of additional people to display in overflow counter badge |
avatarUrls |
string[] | [] |
Array of avatar image URLs |
avatars |
any[] | [] |
Array of avatar objects with imageUrl and optional profileUrl |
className |
string | '' |
Optional CSS class for avatar group container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for container |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.AvatarCircles({ numPeople: 99, avatarUrls: ["https://avatars.githubusercontent.com/u/55315718", "https://avatars.githubusercontent.com/u/20110627", "https://avatars.githubusercontent.com/u/106103625", "https://avatars.githubusercontent.com/u/59228569"] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="demo-wrap" style="align-items:center;justify-content:center;padding:32px 16px;"> <div class="s-avatar-circles"><div class="s-avatar s-avatar--sm"><img class="s-avatar-img" src="assets/avatar-1.svg" alt="Avatar" width="40" height="40" loading="lazy" /></div><div class="s-avatar s-avatar--sm"><img class="s-avatar-img" src="assets/avatar-2.svg" alt="Avatar" width="40" height="40" loading="lazy" /></div><div class="s-avatar s-avatar--sm"><img class="s-avatar-img" src="assets/avatar-3.svg" alt="Avatar" width="40" height="40" loading="lazy" /></div><div class="s-avatar s-avatar--sm"><img class="s-avatar-img" src="assets/avatar-4.svg" alt="Avatar" width="40" height="40" loading="lazy" /></div><span class="s-avatar-circles-count">+99</span></div> </div> Interactive 3D Fibonacci sphere of rotating icons with mouse drag control.
import { IconCloud } from "@/components/sarahui/icon-cloud"; const slugs = [ "typescript", "javascript", "react", "html5", "css3", "nodedotjs", "express", "nextdotjs", "prisma", "amazonaws", "postgresql", "firebase", "nginx", "vercel", "testinglibrary", "jest", "cypress", "docker", "git", "github", "gitlab", "visualstudiocode", "android", "figma"]; export function IconCloudDemo() { return ( <div className="relative flex size-full max-w-lg items-center justify-center overflow-hidden rounded-lg border bg-background px-20 pb-20 pt-8"> <IconCloud iconSlugs={slugs} /> </div> );}
Add IconCloud to your project.
$ npx sarahui add icon-cloud
import { IconCloud } from 'sarahui';
<div className="relative overflow-hidden"> <IconCloud images={images} /></div>
import { IconCloud } from "@/components/sarahui/icon-cloud"; const slugs = [ "typescript", "javascript", "dart", "react", "flutter", "android", "html5", "css", "nodedotjs", "express", "nextdotjs", "prisma", "postgresql", "firebase", "nginx", "vercel", "testinglibrary", "jest", "cypress", "docker", "git", "jira", "github", "gitlab", "androidstudio", "figma", "tailwindcss", "python", "graphql", "mongodb",]; export function IconCloudDemo() { return ( <div className="relative flex size-full max-w-lg items-center justify-center overflow-hidden rounded-lg border bg-background px-20 pb-20 pt-8"> <IconCloud iconSlugs={slugs} /> </div> );}
import { IconCloud } from "@/components/sarahui/icon-cloud"; const images = [ "assets/photo-1.svg", "assets/photo-2.svg", "assets/photo-3.svg", "assets/photo-4.svg", "assets/photo-5.svg", "assets/avatar-5.svg", "assets/avatar-2.svg", "assets/avatar-3.svg", "assets/avatar-4.svg"]; export function IconCloudImagesDemo() { return ( <div className="relative flex size-full max-w-lg items-center justify-center overflow-hidden rounded-lg border bg-background"> <IconCloud images={images} /> </div> );}
Configuration parameters and properties for IconCloud.
| Prop | Type | Default | Description |
|---|---|---|---|
images |
string[] | [] |
Array of direct image URLs to project on the spherical cloud |
iconSlugs |
string[] | [] |
Array of SimpleIcons brand slugs to render |
slugs |
string[] | [] |
Alternative slugs array property |
icons |
string[] | [] |
Alternative icon names array property |
showControl |
boolean | true |
Whether to render interactive pause and resume controls |
className |
string | '' |
Optional CSS class for icon cloud canvas wrapper |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for wrapper element |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.IconCloud({ iconSlugs: ["typescript", "javascript", "dart", "java", "react", "flutter", "android", "html5", "css3", "nodedotjs", "express", "nextdotjs", "prisma", "amazonaws", "postgresql", "firebase", "nginx", "vercel", "docker", "git", "github", "figma"], size: 280 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;justify-content:center;align-items:center;width:100%;"><div class="s-icon-cloud-wrap"><canvas class="s-icon-cloud-canvas" data-s-icon-cloud data-images="["data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(173%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3ETY%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(199%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EJA%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(13%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EDA%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(91%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3ERE%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(86%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EFL%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(293%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EAN%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(156%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EHT%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(0%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3ECS%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(337%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3ENO%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(67%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EEX%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(327%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3ENE%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(42%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EPR%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(39%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EPO%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(83%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EFI%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(329%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3ENG%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(215%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EVE%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(153%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3ETE%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(203%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EJE%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(6%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3ECY%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(27%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EDO%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(114%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EGI%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(207%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EJI%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(114%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EGI%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(114%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EGI%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(293%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EAN%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(83%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EFI%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(149%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3ETA%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(49%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EPY%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(123%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EGR%3C%2Ftext%3E%3C%2Fsvg%3E","data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2216%22%20fill%3D%22hsl(306%2C%2072%25%2C%2052%25)%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2234%22%20fill%3D%22white%22%20font-family%3D%22system-ui%2Csans-serif%22%20font-size%3D%2221%22%20font-weight%3D%22700%22%20text-anchor%3D%22middle%22%3EMO%3C%2Ftext%3E%3C%2Fsvg%3E"]" data-icon-sources="["https://cdn.simpleicons.org/typescript","https://cdn.simpleicons.org/javascript","https://cdn.simpleicons.org/dart","https://cdn.simpleicons.org/react","https://cdn.simpleicons.org/flutter","https://cdn.simpleicons.org/android","https://cdn.simpleicons.org/html5","https://cdn.simpleicons.org/css","https://cdn.simpleicons.org/nodedotjs","https://cdn.simpleicons.org/express","https://cdn.simpleicons.org/nextdotjs","https://cdn.simpleicons.org/prisma","https://cdn.simpleicons.org/postgresql","https://cdn.simpleicons.org/firebase","https://cdn.simpleicons.org/nginx","https://cdn.simpleicons.org/vercel","https://cdn.simpleicons.org/testinglibrary","https://cdn.simpleicons.org/jest","https://cdn.simpleicons.org/cypress","https://cdn.simpleicons.org/docker","https://cdn.simpleicons.org/git","https://cdn.simpleicons.org/jira","https://cdn.simpleicons.org/github","https://cdn.simpleicons.org/gitlab","https://cdn.simpleicons.org/androidstudio","https://cdn.simpleicons.org/figma","https://cdn.simpleicons.org/tailwindcss","https://cdn.simpleicons.org/python","https://cdn.simpleicons.org/graphql","https://cdn.simpleicons.org/mongodb"]" width="340" height="340" role="img" aria-label="Interactive 3D Icon Cloud"></canvas><button type="button" class="s-icon-cloud-ctrl" data-s-cloud-toggle aria-label="Pause animation" title="Pause animation"><span class="s-icon-cloud-ctrl-icon"><svg class="s-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M6 4h4v16H6V4zm8 0h4v16h-4V4z"/></svg></span></button></div></div>Interactive magnifying loupe that zooms in on high-detail card areas.
import { Lens } from "@/components/sarahui/lens"; export function LensDemo() { return ( <div className="relative flex items-center justify-center p-6"> <Lens zoomFactor={2} lensSize={150}> <img src="https://images.unsplash.com/photo-1579546929518-9e396f3cc809?w=800&auto=format&fit=crop&q=80" alt="Lens Demo Image" className="rounded-lg object-cover" /> </Lens> </div> );}
Add Lens to your project.
$ npx sarahui add lens
import { Lens } from 'sarahui';
<Lens> <img src="/images/lens-demo.jpg" alt="Lens Demo" /></Lens>
import { Button } from "@/components/ui/button"import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card"import { Lens } from "@/components/sarahui/lens" export function LensDemo() { return ( <Card className="relative max-w-md shadow-none"> <CardHeader> <Lens zoomFactor={2} lensSize={150} isStatic={false} ariaLabel="Zoom Area" > <img src="assets/photo-7.svg" alt="image placeholder" width={500} height={500} /> </Lens> </CardHeader> <CardContent> <CardTitle className="text-2xl">Your next camp</CardTitle> <CardDescription> See our latest and best camp destinations all across the five continents of the globe. </CardDescription> </CardContent> <CardFooter className="space-x-4"> <Button>Let's go</Button> <Button variant="secondary">Another time</Button> </CardFooter> </Card> )}
import { Button } from "@/components/ui/button"import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card"import { Lens } from "@/components/sarahui/lens" export function LensStatic() { return ( <Card className="relative max-w-md shadow-none"> <CardHeader> <Lens isStatic position={{ x: 260, y: 150 }} zoomFactor={2} lensSize={150}> <img src="assets/photo-7.svg" alt="image placeholder" width={500} height={500} /> </Lens> </CardHeader> <CardContent> <CardTitle className="text-2xl">Your next camp</CardTitle> <CardDescription> See our latest and best camp destinations all across the five continents of the globe. </CardDescription> </CardContent> <CardFooter className="space-x-4"> <Button>Let's go</Button> <Button variant="secondary">Another time</Button> </CardFooter> </Card> )}
import { Button } from "@/components/ui/button"import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card"import { Lens } from "@/components/sarahui/lens" export function LensDefaultPosition() { return ( <Card className="relative max-w-md shadow-none"> <CardHeader> <Lens defaultPosition={{ x: 260, y: 150 }} zoomFactor={2} lensSize={150}> <img src="assets/photo-7.svg" alt="image placeholder" width={500} height={500} /> </Lens> </CardHeader> <CardContent> <CardTitle className="text-2xl">Your next camp</CardTitle> <CardDescription> See our latest and best camp destinations all across the five continents of the globe. </CardDescription> </CardContent> <CardFooter className="space-x-4"> <Button>Let's go</Button> <Button variant="secondary">Another time</Button> </CardFooter> </Card> )}
Configuration parameters and properties for Lens.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
any | - |
Image or media content to inspect with the magnifying lens |
zoomFactor |
number | string | 1.5 |
Magnification zoom multiplier factor |
zoom |
number | string | 1.5 |
Alternative zoom multiplier property |
lensSize |
number | 170 |
Diameter of the circular magnifier loupe in pixels |
isStatic |
boolean | false |
Whether the lens loupe stays at a fixed stationary position |
position |
{ x: number; y: number } | { x: 0, y: 0 } |
Fixed coordinates for static lens positioning |
defaultPosition |
{ x: number; y: number } | { x: 0, y: 0 } |
Initial default position of magnifier |
duration |
number | 0.1 |
Animation transition duration for lens movement |
lensColor |
string | 'black' |
Background fill color of the lens element |
ariaLabel |
string | 'Image magnifier' |
Accessible screen reader label for loupe region |
className |
string | '' |
Optional CSS class for lens container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for container |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Lens({ zoom: 1.8, children: UI.html`<div style="padding:20px;">Inspectable Surface</div>` }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;justify-content:center;width:100%;padding:24px 0;"> <div style="max-width:380px;width:100%;border-radius:16px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);overflow:hidden;box-shadow:0 4px 16px rgba(0,0,0,0.06);"> <div style="padding:16px 16px 0;"> <div class="s-lens-container" data-s-lens data-zoom="2" data-size="150" data-static="false" role="region" aria-label="Zoom Area" tabindex="0" style="--lens-size: 150px; --lens-radius: 75px; --lens-zoom: 2; --lens-duration: 0.15s; --lens-color: black;"><div class="s-lens-target"><img src="assets/photo-7.svg" alt="Camp destination" style="width:100%;height:220px;object-fit:cover;display:block;border-radius:10px;"></div><div class="s-lens-overlay" aria-hidden="true"><div class="s-lens-zoomed"><img src="assets/photo-7.svg" alt="Camp destination" style="width:100%;height:220px;object-fit:cover;display:block;border-radius:10px;"></div></div><div class="s-lens-loupe" aria-hidden="true"></div></div> </div> <div style="padding:16px 20px 20px;"> <h4 style="font-size:18px;font-weight:700;margin:0 0 6px;color:var(--s-text-primary);">Your next camp</h4> <p class="s-small s-muted" style="margin:0 0 16px;line-height:1.5;">See our latest and best camp destinations all across the five continents of the globe.</p> <div style="display:flex;gap:10px;"> <button type="button" class="demo-login-btn-primary" style="padding:8px 16px;font-size:13px;border-radius:8px;">Let's go</button> <button type="button" class="demo-login-btn-outline" style="padding:8px 16px;font-size:13px;border-radius:8px;">Another time</button> </div> </div> </div> </div> Multiplayer real-time live cursor badge with customizable name and color.
import { Pointer } from "@/components/sarahui/pointer"; export function PointerDemo() { return ( <div className="relative flex h-[350px] w-full items-center justify-center overflow-hidden rounded-lg border bg-background"> <Pointer name="Sarah" color="#5B3DF5"> <div className="rounded-xl border border-dashed p-8 text-center"> <p className="text-lg font-medium text-foreground">Move your cursor over this card</p> <p className="text-sm text-muted-foreground">Custom collaborative pointer follows motion</p> </div> </Pointer> </div> );}
Add Pointer to your project.
$ npx sarahui add pointer
import { Pointer } from 'sarahui';
<Pointer />
import { Pointer } from "@/components/sarahui/pointer"; export function PointerDemo() { return ( <div className="relative h-64 w-full border rounded-xl"> <Pointer name="Sarah Khan" color="#5B3DF5" /> </div> );}
import { Pointer } from "@/components/sarahui/pointer"; export function PointerMultiplayer() { return ( <div className="relative h-64 w-full"> <Pointer name="Alex" color="#5B3DF5" x={40} y={30} /> <Pointer name="Jamie" color="#10B981" x={180} y={80} /> </div> );}
Configuration parameters and properties for Pointer.
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string | '' |
Name label displayed next to cursor pointer badge |
color |
string | '' |
Custom accent color for cursor glyph and badge |
cursor |
any | - |
Custom SVG cursor glyph or icon component |
children |
any | - |
Content wrapped inside interactive pointer area |
content |
any | - |
Alternative pointer badge content |
className |
string | '' |
Optional CSS class for pointer container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for pointer element |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Pointer({ label: "Sarah Khan", color: "#5B3DF5" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:grid;grid-template-columns:repeat(2,1fr);gap:16px;width:100%;max-width:580px;margin:0 auto;"> <div class="s-custom-pointer-wrap" data-s-pointer><div style="position:relative;height:140px;border-radius:14px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:flex;flex-direction:column;align-items:center;justify-content:center;"><strong style="font-size:14px;">Animated Pointer</strong><span class="s-small s-muted" style="margin-top:2px;">Heart cursor</span></div><div class="s-pointer-glyph" aria-hidden="true"><svg width="24" height="24" viewBox="0 0 24 24" fill="#EC4899" style="animation:s-pulse-heart 1.2s ease-in-out infinite;"><path d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/></svg></div></div> <div class="s-custom-pointer-wrap" data-s-pointer><div style="position:relative;height:140px;border-radius:14px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:flex;flex-direction:column;align-items:center;justify-content:center;"><strong style="font-size:14px;">Colored Pointer</strong><span class="s-small s-muted" style="margin-top:2px;">Default blue pointer</span></div><div class="s-pointer-glyph" aria-hidden="true"><svg class="s-pointer-cursor" width="22" height="22" viewBox="0 0 16 16" fill="#3B82F6" style="transform:rotate(-70deg);filter:drop-shadow(0 2px 4px rgba(0,0,0,0.25));"><path d="M14.082 2.182a.5.5 0 0 1 .103.557L8.528 15.467a.5.5 0 0 1-.917-.007L5.57 10.694.803 8.652a.5.5 0 0 1-.006-.916l12.728-5.657a.5.5 0 0 1 .556.103z"/></svg></div></div> <div class="s-custom-pointer-wrap" data-s-pointer><div style="position:relative;height:140px;border-radius:14px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:flex;flex-direction:column;align-items:center;justify-content:center;"><strong style="font-size:14px;">Custom Shape</strong><span class="s-small s-muted" style="margin-top:2px;">Target circle cursor</span></div><div class="s-pointer-glyph" aria-hidden="true"><svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="8" fill="none" stroke="#8B5CF6" stroke-width="2"/><circle cx="10" cy="10" r="3" fill="#8B5CF6"/></svg></div></div> <div class="s-custom-pointer-wrap" data-s-pointer><div style="position:relative;height:140px;border-radius:14px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:flex;flex-direction:column;align-items:center;justify-content:center;"><strong style="font-size:14px;">Emoji Pointer</strong><span class="s-small s-muted" style="margin-top:2px;">Finger emoji cursor</span></div><div class="s-pointer-glyph" aria-hidden="true"><span style="font-size:24px;line-height:1;">👆</span></div></div> </div> Fluid spring-physics cursor follower creating a responsive pointer trail.
import { SmoothCursor } from "@/components/sarahui/smooth-cursor"; export function SmoothCursorDemo() { return ( <div className="relative flex h-[350px] w-full items-center justify-center overflow-hidden rounded-lg border bg-background"> <SmoothCursor color="#5B3DF5" /> <div className="text-center"> <h3 className="text-xl font-semibold">Smooth Cursor Active</h3> <p className="text-sm text-muted-foreground">Spring physics with zero global pointer lock</p> </div> </div> );}
Add SmoothCursor to your project.
$ npx sarahui add smooth-cursor
import { SmoothCursor } from 'sarahui';
<SmoothCursor />
To prevent the default browser cursor from overlapping with the custom cursor, add the following CSS globally:
* { cursor: none !important;}
To preserve the standard text selection I-beam cursor when hovering over input fields, text areas, and select menus:
input,textarea,select { cursor: text !important;}
If you're using Tailwind CSS, you can apply cursor-none to your layout wrapper or body element to cleanly hide the default browser pointer:
<div className="cursor-none"> <SmoothCursor /> {/* your app */}</div>
import { SmoothCursor } from "@/components/sarahui/smooth-cursor"; export default function SmoothCursorDemo() { return ( <> <span className="hidden md:block">Move your mouse around</span> <span className="block md:hidden"> SmoothCursor is disabled on touch devices </span> <SmoothCursor /> </> );}
Configuration parameters and properties for SmoothCursor.
| Prop | Type | Default | Description |
|---|---|---|---|
cursor |
any | <DefaultCursorSVG /> |
Custom cursor component or SVG glyph |
color |
string | 'currentColor' |
Fill color for default cursor glyph |
className |
string | '' |
Optional CSS class for cursor container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for cursor element |
Customize the smooth cursor inertia, tension, and friction dynamics with spring physics.
interface SpringConfig { damping: number; // Controls how quickly the animation settles stiffness: number; // Controls the spring stiffness mass: number; // Controls the virtual mass of the cursor restDelta: number; // Threshold to consider animation complete}
const defaultSpringConfig = { damping: 45, stiffness: 400, mass: 1, restDelta: 0.001,};
Compatible with all modern desktop and laptop browsers supporting:
requestAnimationFrame for 60fps/120fps hardware-accelerated movementtranslate3d, rotate, scale)pointermove, pointerleave)Touch screens, tablets, and mobile devices with coarse pointers are automatically bypassed. This prevents the custom animated cursor from getting stuck or popping up after touch taps, keeping native mobile gestures smooth and fluid.
Inclusive motion design and assistive technology considerations:
aria-hidden="true").prefers-reduced-motion: reduce to ensure users with vestibular sensitivities have a comfortable browsing experience.Engineered as a native SarahUI progressive enhancement with spring physics and zero global pointer lock.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.SmoothCursor({ color: "#5B3DF5" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-smooth-cursor-box"> <span class="s-smooth-cursor-desktop-text">Move your mouse around</span> <span class="s-smooth-cursor-touch-text">SmoothCursor is disabled on touch devices</span> <div class="s-smooth-cursor-container" data-s-smooth-cursor aria-hidden="true"><div class="s-smooth-cursor-follower"><svg xmlns="http://www.w3.org/2000/svg" width="50" height="54" viewBox="0 0 50 54" fill="none" style="transform: scale(0.5); transform-origin: center center; display: block;"><g filter="url(#s-cursor-filter-4)"><path d="M42.6817 41.1495L27.5103 6.79925C26.7269 5.02557 24.2082 5.02558 23.3927 6.79925L7.59814 41.1495C6.75833 42.9759 8.52712 44.8902 10.4125 44.1954L24.3757 39.0496C24.8829 38.8627 25.4385 38.8627 25.9422 39.0496L39.8121 44.1954C41.6849 44.8902 43.4884 42.9759 42.6817 41.1495Z" fill="currentColor"/><path d="M43.7146 40.6933L28.5431 6.34306C27.3556 3.65428 23.5772 3.69516 22.3668 6.32755L6.57226 40.6778C5.3134 43.4156 7.97238 46.298 10.803 45.2549L24.7662 40.109C25.0221 40.0147 25.2999 40.0156 25.5494 40.1082L39.4193 45.254C42.2261 46.2953 44.9254 43.4347 43.7146 40.6933Z" stroke="var(--s-bg-canvas, #0B0D13)" stroke-width="2.25825"/></g><defs><filter id="s-cursor-filter-4" x="0.602397" y="0.952444" width="49.0584" height="52.428" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/><feOffset dy="2.25825"/><feGaussianBlur stdDeviation="2.25825"/><feComposite in2="hardAlpha" operator="out"/><feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.18 0"/><feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/><feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/></filter></defs></svg></div></div> </div> Multi-stop progressive gradient backdrop blur filter.
import { ProgressiveBlur } from "@/components/sarahui/progressive-blur"; export function ProgressiveBlurDemo() { return ( <div className="relative h-[350px] w-full overflow-hidden rounded-xl border bg-background"> <div className="space-y-4 p-6"> <h3 className="text-xl font-bold">Progressive Blur Gradient</h3> <p className="text-muted-foreground"> Multi-layer Gaussian blur creates smooth visual depth and content fade. </p> </div> <ProgressiveBlur position="bottom" height="40%" /> </div> );}
Add ProgressiveBlur to your project.
$ npx sarahui add progressive-blur
import { ProgressiveBlur } from 'sarahui';
<div className="relative h-[400px] w-full overflow-auto"> {/* Your scrollable content */} <div className="space-y-4 p-4">{/* Content items */}</div> <ProgressiveBlur height="50%" position="bottom" /></div>
import { ProgressiveBlur } from "@/components/sarahui/progressive-blur"; export function ProgressiveBlurDemo() { return ( <div className="relative h-80 w-full overflow-hidden rounded-xl border"> <div className="h-full overflow-y-auto p-4 space-y-2"> {/* Scrollable feed items */} </div> <ProgressiveBlur position="bottom" height="35%" /> </div> );}
import { ProgressiveBlur } from "@/components/sarahui/progressive-blur"; export function ProgressiveBlurDual() { return ( <div className="relative h-72 w-full overflow-hidden rounded-xl border"> <div className="h-full overflow-y-auto p-4 space-y-2"> {/* Terminal logs or feed */} </div> <ProgressiveBlur position="both" height="28%" /> </div> );}
import { ProgressiveBlur } from "@/components/sarahui/progressive-blur"; export function ProgressiveBlurHero() { return ( <div className="relative h-80 w-full overflow-hidden rounded-xl border bg-gradient-to-br from-indigo-500/30 to-purple-500/20 p-6 flex items-end"> <div className="relative z-10 w-full p-4 rounded-xl bg-background/80 border backdrop-blur-md"> <h4 className="font-semibold text-sm">Liquid Glassmorphism</h4> <p className="text-xs text-muted-foreground">Smooth optical blur overlay.</p> </div> <ProgressiveBlur position="bottom" height="55%" /> </div> );}
Configuration parameters and properties for ProgressiveBlur.
| Prop | Type | Default | Description |
|---|---|---|---|
position |
'top' | 'bottom' | 'left' | 'right' | 'both' | 'bottom' |
Edge where the progressive blur gradient is anchored |
direction |
'top' | 'bottom' | 'left' | 'right' | 'both' | 'bottom' |
Alternative direction property |
height |
number | string | '30%' |
Height (or width) coverage of the progressive blur overlay |
blurLevels |
number[] | [0.5, 1, 2, 4, 8, 16, 32, 64] |
Array of blur radii (in px) defining the multi-layer Gaussian blur curve |
children |
any | - |
Optional scrollable content wrapped within blur overlay |
className |
string | '' |
Optional CSS class for blur overlay container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for container |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.ProgressiveBlur({ direction: "bottom" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:380px;width:100%;max-width:460px;margin:0 auto;border-radius:18px;border:1px solid var(--s-glass-border, var(--s-border-default));background:var(--s-glass-bg, var(--s-bg-surface));backdrop-filter:var(--s-glass-blur, blur(16px));-webkit-backdrop-filter:var(--s-glass-blur, blur(16px));overflow:hidden;display:flex;flex-direction:column;"> <div style="display:flex;align-items:center;justify-content:space-between;padding:12px 18px;border-bottom:1px solid var(--s-glass-border, var(--s-border-default));background:var(--s-glass-bg-subtle, var(--s-bg-subtle));z-index:20;flex-shrink:0;"> <div style="display:flex;align-items:center;gap:8px;"> <span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:#10b981;box-shadow:0 0 8px rgba(16,185,129,0.6);"></span> <strong style="font-size:13px;font-weight:600;color:var(--s-text-primary);">Real-time Activity Stream</strong> </div> <span style="font:10px var(--s-font-mono),monospace;color:var(--s-text-muted);background:var(--s-glass-bg, var(--s-bg-surface));border:1px solid var(--s-glass-border, var(--s-border-default));padding:2px 7px;border-radius:6px;">Live</span> </div> <div style="overflow-y:auto;flex:1;padding:16px 14px;display:flex;flex-direction:column;gap:10px;"> <div style="display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border-radius:12px;background:var(--s-glass-bg, var(--s-bg-subtle));border:1px solid var(--s-glass-border, var(--s-border-default));transition:transform var(--s-duration-fast);box-sizing:border-box;"> <div style="width:32px;height:32px;border-radius:9px;background:color-mix(in srgb, #8b5cf6 15%, transparent);color:#8b5cf6;display:grid;place-items:center;flex-shrink:0;border:1px solid color-mix(in srgb, #8b5cf6 30%, transparent);"> <svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 2 2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5L2 12l7.5-2.5Z"/></svg> </div> <div style="flex:1;min-width:0;"> <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:2px;"> <strong style="font-size:12.5px;font-weight:600;color:var(--s-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">Edge Deployment Complete</strong> <span style="font-size:11px;color:var(--s-text-muted);white-space:nowrap;flex-shrink:0;">Just now</span> </div> <p style="font-size:11.5px;color:var(--s-text-muted);margin:0;line-height:1.45;">Production v2.4.0 active in 12 global regions.</p> </div> </div> <div style="display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border-radius:12px;background:var(--s-glass-bg, var(--s-bg-subtle));border:1px solid var(--s-glass-border, var(--s-border-default));transition:transform var(--s-duration-fast);box-sizing:border-box;"> <div style="width:32px;height:32px;border-radius:9px;background:color-mix(in srgb, #10b981 15%, transparent);color:#10b981;display:grid;place-items:center;flex-shrink:0;border:1px solid color-mix(in srgb, #10b981 30%, transparent);"> <svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4 12 5 5L20 6"/></svg> </div> <div style="flex:1;min-width:0;"> <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:2px;"> <strong style="font-size:12.5px;font-weight:600;color:var(--s-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">Payment Confirmed</strong> <span style="font-size:11px;color:var(--s-text-muted);white-space:nowrap;flex-shrink:0;">4m ago</span> </div> <p style="font-size:11.5px;color:var(--s-text-muted);margin:0;line-height:1.45;">Enterprise annual plan renewed via Stripe ($2,400.00).</p> </div> </div> <div style="display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border-radius:12px;background:var(--s-glass-bg, var(--s-bg-subtle));border:1px solid var(--s-glass-border, var(--s-border-default));transition:transform var(--s-duration-fast);box-sizing:border-box;"> <div style="width:32px;height:32px;border-radius:9px;background:color-mix(in srgb, #ec4899 15%, transparent);color:#ec4899;display:grid;place-items:center;flex-shrink:0;border:1px solid color-mix(in srgb, #ec4899 30%, transparent);"> <svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21 3.5 12.5a5.3 5.3 0 0 1 7.5-7.5l1 1 1-1a5.3 5.3 0 0 1 7.5 7.5Z"/></svg> </div> <div style="flex:1;min-width:0;"> <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:2px;"> <strong style="font-size:12.5px;font-weight:600;color:var(--s-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">New GitHub Star</strong> <span style="font-size:11px;color:var(--s-text-muted);white-space:nowrap;flex-shrink:0;">18m ago</span> </div> <p style="font-size:11.5px;color:var(--s-text-muted);margin:0;line-height:1.45;">sarahUI reached 22,350+ GitHub stars worldwide.</p> </div> </div> <div style="display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border-radius:12px;background:var(--s-glass-bg, var(--s-bg-subtle));border:1px solid var(--s-glass-border, var(--s-border-default));transition:transform var(--s-duration-fast);box-sizing:border-box;"> <div style="width:32px;height:32px;border-radius:9px;background:color-mix(in srgb, #06b6d4 15%, transparent);color:#06b6d4;display:grid;place-items:center;flex-shrink:0;border:1px solid color-mix(in srgb, #06b6d4 30%, transparent);"> <svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 18h16M8 3v6m8 6v6M4 12h16M15 9v6"/></svg> </div> <div style="flex:1;min-width:0;"> <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:2px;"> <strong style="font-size:12.5px;font-weight:600;color:var(--s-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">Performance Benchmark</strong> <span style="font-size:11px;color:var(--s-text-muted);white-space:nowrap;flex-shrink:0;">42m ago</span> </div> <p style="font-size:11.5px;color:var(--s-text-muted);margin:0;line-height:1.45;">LCP latency dropped to 0.38s across all core vitals.</p> </div> </div> <div style="display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border-radius:12px;background:var(--s-glass-bg, var(--s-bg-subtle));border:1px solid var(--s-glass-border, var(--s-border-default));transition:transform var(--s-duration-fast);box-sizing:border-box;"> <div style="width:32px;height:32px;border-radius:9px;background:color-mix(in srgb, #f59e0b 15%, transparent);color:#f59e0b;display:grid;place-items:center;flex-shrink:0;border:1px solid color-mix(in srgb, #f59e0b 30%, transparent);"> <svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h7l2 3h9v12H3Zm0 0V3h7l2 3h9v3"/></svg> </div> <div style="flex:1;min-width:0;"> <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:2px;"> <strong style="font-size:12.5px;font-weight:600;color:var(--s-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">Database Backup Synced</strong> <span style="font-size:11px;color:var(--s-text-muted);white-space:nowrap;flex-shrink:0;">1h ago</span> </div> <p style="font-size:11.5px;color:var(--s-text-muted);margin:0;line-height:1.45;">Multi-region Postgres snapshot created in eu-central-1.</p> </div> </div> <div style="display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border-radius:12px;background:var(--s-glass-bg, var(--s-bg-subtle));border:1px solid var(--s-glass-border, var(--s-border-default));transition:transform var(--s-duration-fast);box-sizing:border-box;"> <div style="width:32px;height:32px;border-radius:9px;background:color-mix(in srgb, #6366f1 15%, transparent);color:#6366f1;display:grid;place-items:center;flex-shrink:0;border:1px solid color-mix(in srgb, #6366f1 30%, transparent);"> <svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 2 2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5L2 12l7.5-2.5Z"/></svg> </div> <div style="flex:1;min-width:0;"> <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:2px;"> <strong style="font-size:12.5px;font-weight:600;color:var(--s-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">SSL Certificate Renewed</strong> <span style="font-size:11px;color:var(--s-text-muted);white-space:nowrap;flex-shrink:0;">2h ago</span> </div> <p style="font-size:11.5px;color:var(--s-text-muted);margin:0;line-height:1.45;">Wildcard TLS automated encryption renewed successfully.</p> </div> </div> <div style="display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border-radius:12px;background:var(--s-glass-bg, var(--s-bg-subtle));border:1px solid var(--s-glass-border, var(--s-border-default));transition:transform var(--s-duration-fast);box-sizing:border-box;"> <div style="width:32px;height:32px;border-radius:9px;background:color-mix(in srgb, #10b981 15%, transparent);color:#10b981;display:grid;place-items:center;flex-shrink:0;border:1px solid color-mix(in srgb, #10b981 30%, transparent);"> <svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4 12 5 5L20 6"/></svg> </div> <div style="flex:1;min-width:0;"> <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:2px;"> <strong style="font-size:12.5px;font-weight:600;color:var(--s-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">Security Policy Passed</strong> <span style="font-size:11px;color:var(--s-text-muted);white-space:nowrap;flex-shrink:0;">4h ago</span> </div> <p style="font-size:11.5px;color:var(--s-text-muted);margin:0;line-height:1.45;">Zero vulnerabilities reported across 75 unit audits.</p> </div> </div> <div style="display:flex;align-items:flex-start;gap:12px;padding:12px 14px;border-radius:12px;background:var(--s-glass-bg, var(--s-bg-subtle));border:1px solid var(--s-glass-border, var(--s-border-default));transition:transform var(--s-duration-fast);box-sizing:border-box;"> <div style="width:32px;height:32px;border-radius:9px;background:color-mix(in srgb, #3b82f6 15%, transparent);color:#3b82f6;display:grid;place-items:center;flex-shrink:0;border:1px solid color-mix(in srgb, #3b82f6 30%, transparent);"> <svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h7l2 3h9v12H3Zm0 0V3h7l2 3h9v3"/></svg> </div> <div style="flex:1;min-width:0;"> <div style="display:flex;align-items:center;justify-content:space-between;gap:8px;margin-bottom:2px;"> <strong style="font-size:12.5px;font-weight:600;color:var(--s-text-primary);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">CDN Cache Purged</strong> <span style="font-size:11px;color:var(--s-text-muted);white-space:nowrap;flex-shrink:0;">6h ago</span> </div> <p style="font-size:11.5px;color:var(--s-text-muted);margin:0;line-height:1.45;">Global edge cache refreshed in 45ms across 180 POPS.</p> </div> </div> </div> <div class="s-progressive-blur-container s-progressive-blur-both"><div class="s-progressive-blur s-progressive-blur--top" style="--blur-height:28%;" aria-hidden="true"><div class="s-progressive-blur-layer" style="z-index:1;backdrop-filter:blur(0.5px);-webkit-backdrop-filter:blur(0.5px);mask-image:linear-gradient(to top, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12.5%, rgba(0,0,0,1) 25.0%, rgba(0,0,0,0) 37.5%);-webkit-mask-image:linear-gradient(to top, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12.5%, rgba(0,0,0,1) 25.0%, rgba(0,0,0,0) 37.5%);"></div><div class="s-progressive-blur-layer" style="z-index:2;backdrop-filter:blur(1px);-webkit-backdrop-filter:blur(1px);mask-image:linear-gradient(to top, rgba(0,0,0,0) 12.5%, rgba(0,0,0,1) 25.0%, rgba(0,0,0,1) 37.5%, rgba(0,0,0,0) 50.0%);-webkit-mask-image:linear-gradient(to top, rgba(0,0,0,0) 12.5%, rgba(0,0,0,1) 25.0%, rgba(0,0,0,1) 37.5%, rgba(0,0,0,0) 50.0%);"></div><div class="s-progressive-blur-layer" style="z-index:3;backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);mask-image:linear-gradient(to top, rgba(0,0,0,0) 25.0%, rgba(0,0,0,1) 37.5%, rgba(0,0,0,1) 50.0%, rgba(0,0,0,0) 62.5%);-webkit-mask-image:linear-gradient(to top, rgba(0,0,0,0) 25.0%, rgba(0,0,0,1) 37.5%, rgba(0,0,0,1) 50.0%, rgba(0,0,0,0) 62.5%);"></div><div class="s-progressive-blur-layer" style="z-index:4;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);mask-image:linear-gradient(to top, rgba(0,0,0,0) 37.5%, rgba(0,0,0,1) 50.0%, rgba(0,0,0,1) 62.5%, rgba(0,0,0,0) 75.0%);-webkit-mask-image:linear-gradient(to top, rgba(0,0,0,0) 37.5%, rgba(0,0,0,1) 50.0%, rgba(0,0,0,1) 62.5%, rgba(0,0,0,0) 75.0%);"></div><div class="s-progressive-blur-layer" style="z-index:5;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);mask-image:linear-gradient(to top, rgba(0,0,0,0) 50.0%, rgba(0,0,0,1) 62.5%, rgba(0,0,0,1) 75.0%, rgba(0,0,0,0) 87.5%);-webkit-mask-image:linear-gradient(to top, rgba(0,0,0,0) 50.0%, rgba(0,0,0,1) 62.5%, rgba(0,0,0,1) 75.0%, rgba(0,0,0,0) 87.5%);"></div><div class="s-progressive-blur-layer" style="z-index:6;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);mask-image:linear-gradient(to top, rgba(0,0,0,0) 62.5%, rgba(0,0,0,1) 75.0%, rgba(0,0,0,1) 87.5%, rgba(0,0,0,0) 100.0%);-webkit-mask-image:linear-gradient(to top, rgba(0,0,0,0) 62.5%, rgba(0,0,0,1) 75.0%, rgba(0,0,0,1) 87.5%, rgba(0,0,0,0) 100.0%);"></div><div class="s-progressive-blur-layer" style="z-index:7;backdrop-filter:blur(32px);-webkit-backdrop-filter:blur(32px);mask-image:linear-gradient(to top, rgba(0,0,0,0) 75.0%, rgba(0,0,0,1) 87.5%, rgba(0,0,0,1) 100.0%, rgba(0,0,0,0) 100.0%);-webkit-mask-image:linear-gradient(to top, rgba(0,0,0,0) 75.0%, rgba(0,0,0,1) 87.5%, rgba(0,0,0,1) 100.0%, rgba(0,0,0,0) 100.0%);"></div><div class="s-progressive-blur-layer" style="z-index:8;backdrop-filter:blur(64px);-webkit-backdrop-filter:blur(64px);mask-image:linear-gradient(to top, rgba(0,0,0,0) 87.5%, rgba(0,0,0,1) 100%);-webkit-mask-image:linear-gradient(to top, rgba(0,0,0,0) 87.5%, rgba(0,0,0,1) 100%);"></div></div><div class="s-progressive-blur s-progressive-blur--bottom" style="--blur-height:28%;" aria-hidden="true"><div class="s-progressive-blur-layer" style="z-index:1;backdrop-filter:blur(0.5px);-webkit-backdrop-filter:blur(0.5px);mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12.5%, rgba(0,0,0,1) 25.0%, rgba(0,0,0,0) 37.5%);-webkit-mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 12.5%, rgba(0,0,0,1) 25.0%, rgba(0,0,0,0) 37.5%);"></div><div class="s-progressive-blur-layer" style="z-index:2;backdrop-filter:blur(1px);-webkit-backdrop-filter:blur(1px);mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 12.5%, rgba(0,0,0,1) 25.0%, rgba(0,0,0,1) 37.5%, rgba(0,0,0,0) 50.0%);-webkit-mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 12.5%, rgba(0,0,0,1) 25.0%, rgba(0,0,0,1) 37.5%, rgba(0,0,0,0) 50.0%);"></div><div class="s-progressive-blur-layer" style="z-index:3;backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px);mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 25.0%, rgba(0,0,0,1) 37.5%, rgba(0,0,0,1) 50.0%, rgba(0,0,0,0) 62.5%);-webkit-mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 25.0%, rgba(0,0,0,1) 37.5%, rgba(0,0,0,1) 50.0%, rgba(0,0,0,0) 62.5%);"></div><div class="s-progressive-blur-layer" style="z-index:4;backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 37.5%, rgba(0,0,0,1) 50.0%, rgba(0,0,0,1) 62.5%, rgba(0,0,0,0) 75.0%);-webkit-mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 37.5%, rgba(0,0,0,1) 50.0%, rgba(0,0,0,1) 62.5%, rgba(0,0,0,0) 75.0%);"></div><div class="s-progressive-blur-layer" style="z-index:5;backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 50.0%, rgba(0,0,0,1) 62.5%, rgba(0,0,0,1) 75.0%, rgba(0,0,0,0) 87.5%);-webkit-mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 50.0%, rgba(0,0,0,1) 62.5%, rgba(0,0,0,1) 75.0%, rgba(0,0,0,0) 87.5%);"></div><div class="s-progressive-blur-layer" style="z-index:6;backdrop-filter:blur(16px);-webkit-backdrop-filter:blur(16px);mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 62.5%, rgba(0,0,0,1) 75.0%, rgba(0,0,0,1) 87.5%, rgba(0,0,0,0) 100.0%);-webkit-mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 62.5%, rgba(0,0,0,1) 75.0%, rgba(0,0,0,1) 87.5%, rgba(0,0,0,0) 100.0%);"></div><div class="s-progressive-blur-layer" style="z-index:7;backdrop-filter:blur(32px);-webkit-backdrop-filter:blur(32px);mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 75.0%, rgba(0,0,0,1) 87.5%, rgba(0,0,0,1) 100.0%, rgba(0,0,0,0) 100.0%);-webkit-mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 75.0%, rgba(0,0,0,1) 87.5%, rgba(0,0,0,1) 100.0%, rgba(0,0,0,0) 100.0%);"></div><div class="s-progressive-blur-layer" style="z-index:8;backdrop-filter:blur(64px);-webkit-backdrop-filter:blur(64px);mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 87.5%, rgba(0,0,0,1) 100%);-webkit-mask-image:linear-gradient(to bottom, rgba(0,0,0,0) 87.5%, rgba(0,0,0,1) 100%);"></div></div></div> </div> Minimalist dotted world vector map with pulsating geographic location markers.
import { DottedMap } from "@/components/sarahui/dotted-map"; const markers = [ { lat: 40.7128, lng: -74.006, label: "New York", size: 4 }, { lat: 51.5074, lng: -0.1278, label: "London", size: 4 }, { lat: 35.6762, lng: 139.6503, label: "Tokyo", size: 4 }, { lat: -33.8688, lng: 151.2093, label: "Sydney", size: 4 },]; export function DottedMapDemo() { return ( <div className="relative h-[400px] w-full overflow-hidden rounded-xl border bg-background"> <DottedMap markers={markers} dotColor="var(--s-brand-indigo, #5B3DF5)" /> </div> );}
Add DottedMap to your project.
$ npx sarahui add dotted-map
import { DottedMap } from 'sarahui';
<div className="relative h-[400px] w-full overflow-hidden rounded-xl border"> <DottedMap /></div>
import { DottedMap } from "@/components/sarahui/dotted-map"; export function DottedMapDemo() { const markers = [ { lat: 37.7749, lng: -122.4194, label: "San Francisco", country: "us", pulse: true }, { lat: 40.7128, lng: -74.0060, label: "New York", country: "us", pulse: true }, { lat: 51.5074, lng: -0.1278, label: "London", country: "gb", pulse: true }, { lat: 25.2048, lng: 55.2708, label: "Dubai", country: "ae", pulse: true }, { lat: 23.8103, lng: 90.4125, label: "Dhaka", country: "bd", pulse: true }, { lat: 35.6762, lng: 139.6503, label: "Tokyo", country: "jp", pulse: true }, { lat: 1.3521, lng: 103.8198, label: "Singapore", country: "sg", pulse: true }, { lat: -33.8688, lng: 151.2093, label: "Sydney", country: "au", pulse: true } ]; const lines = [ { from: { lat: 37.7749, lng: -122.4194 }, to: { lat: 51.5074, lng: -0.1278 } }, { from: { lat: 51.5074, lng: -0.1278 }, to: { lat: 25.2048, lng: 55.2708 } }, { from: { lat: 25.2048, lng: 55.2708 }, to: { lat: 23.8103, lng: 90.4125 } }, { from: { lat: 23.8103, lng: 90.4125 }, to: { lat: 1.3521, lng: 103.8198 } }, { from: { lat: 1.3521, lng: 103.8198 }, to: { lat: -33.8688, lng: 151.2093 } }, { from: { lat: 51.5074, lng: -0.1278 }, to: { lat: 35.6762, lng: 139.6503 } }, { from: { lat: 35.6762, lng: 139.6503 }, to: { lat: 37.7749, lng: -122.4194 } } ]; return ( <div className="relative flex h-[420px] w-full items-center justify-center overflow-hidden rounded-xl border bg-background"> <DottedMap markers={markers} lines={lines} /> </div> );}
import { DottedMap } from "@/components/sarahui/dotted-map"; export function DottedMapPulse() { const markers = [ { lat: 37.7749, lng: -122.4194, size: 3.5, pulse: true, color: "#3B82F6" }, { lat: 51.5074, lng: -0.1278, size: 3.5, pulse: true, color: "#10B981" }, { lat: 35.6762, lng: 139.6503, size: 3.5, pulse: true, color: "#F59E0B" }, { lat: -33.8688, lng: 151.2093, size: 3.5, pulse: true, color: "#EC4899" }, { lat: 23.8103, lng: 90.4125, size: 3.5, pulse: true, color: "#8B5CF6" } ]; return <DottedMap markers={markers} />;}
import { DottedMap } from "@/components/sarahui/dotted-map"; export function DottedMapMatrix() { return ( <DottedMap dotRadius={1.2} dotColor="#5B3DF5" markerColor="#10B981" markers={[ { lat: 40.7128, lng: -74.0060, label: "US-East", pulse: true }, { lat: 50.1109, lng: 8.6821, label: "EU-Central", pulse: true }, { lat: 1.3521, lng: 103.8198, label: "AP-Southeast", pulse: true } ]} /> );}
Configuration parameters and properties for DottedMap.
| Prop | Type | Default | Description |
|---|---|---|---|
width |
number | 800 |
Width of the SVG viewport coordinate grid |
height |
number | 400 |
Height of the SVG viewport coordinate grid |
dotRadius |
number | 1.6 |
Radius (in px) of individual geographic map dots |
dotSize |
number | 1.6 |
Alternative dot size property |
dotColor |
string | 'currentColor' |
Fill color for continent dots (adapts to theme) |
markerColor |
string | '#FF6900' |
Accent color for marker beacons and radar rings |
markers |
MapMarker[] | [] |
Array of geographic markers with lat/lng coordinates and size |
points |
MapMarker[] | [] |
Alternative geographic points array |
lines |
any[] | [] |
Array of connection paths/routes between geographic coordinates |
routes |
any[] | [] |
Alternative routes array |
connections |
any[] | [] |
Alternative connection lines array |
glow |
boolean | true |
Whether to render subtle radial vignette glow backdrop |
className |
string | '' |
Optional CSS class for map container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for map container |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.DottedMap({ points: [{ x: 25, y: 35, label: "SF" }] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;width:100%;max-width:820px;height:420px;margin:0 auto;display:flex;align-items:center;justify-content:center;overflow:hidden;border-radius:16px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);box-shadow:var(--s-shadow-sm, 0 4px 12px rgba(0,0,0,0.05));"> <div class="s-dotted-map-container" data-s-dotted-map><svg class="s-dotted-map" viewBox="0 0 800 400" width="100%" height="100%" aria-hidden="true"> <defs> <linearGradient id="s-map-6-flight-gradient" x1="0%" y1="0%" x2="100%" y2="100%"> <stop offset="0%" stop-color="#FF6900" stop-opacity="0.9"/> <stop offset="100%" stop-color="var(--s-color-brand-500, #5B3DF5)" stop-opacity="0.9"/> </linearGradient> </defs> <g class="s-map-dots" opacity="0.45"><circle cx="284.0" cy="4.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="4.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="4.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="4.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="4.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="4.0" r="1.6" fill="currentColor"/><circle cx="332.0" cy="4.0" r="1.6" fill="currentColor"/><circle cx="340.0" cy="4.0" r="1.6" fill="currentColor"/><circle cx="348.0" cy="4.0" r="1.6" fill="currentColor"/><circle cx="356.0" cy="4.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="4.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="4.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="12.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="12.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="12.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="12.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="12.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="12.0" r="1.6" fill="currentColor"/><circle cx="332.0" cy="12.0" r="1.6" fill="currentColor"/><circle cx="340.0" cy="12.0" r="1.6" fill="currentColor"/><circle cx="348.0" cy="12.0" r="1.6" fill="currentColor"/><circle cx="356.0" cy="12.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="12.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="12.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="332.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="340.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="348.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="356.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="764.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="772.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="780.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="788.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="796.0" cy="20.0" r="1.6" fill="currentColor"/><circle cx="100.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="108.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="116.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="124.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="132.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="332.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="340.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="348.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="356.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="748.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="756.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="764.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="772.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="780.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="788.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="796.0" cy="28.0" r="1.6" fill="currentColor"/><circle cx="36.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="44.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="52.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="60.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="68.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="76.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="84.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="92.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="100.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="108.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="116.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="124.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="132.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="332.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="340.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="348.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="356.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="748.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="756.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="764.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="772.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="780.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="788.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="796.0" cy="36.0" r="1.6" fill="currentColor"/><circle cx="36.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="44.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="52.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="60.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="68.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="76.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="84.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="92.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="100.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="108.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="116.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="124.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="132.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="196.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="204.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="212.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="332.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="340.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="348.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="356.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="748.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="756.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="764.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="772.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="780.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="788.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="796.0" cy="44.0" r="1.6" fill="currentColor"/><circle cx="36.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="44.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="52.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="60.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="68.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="76.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="84.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="92.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="100.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="108.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="116.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="124.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="132.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="356.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="748.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="756.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="764.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="772.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="780.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="788.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="796.0" cy="52.0" r="1.6" fill="currentColor"/><circle cx="36.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="44.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="52.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="60.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="68.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="76.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="84.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="92.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="100.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="108.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="116.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="124.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="132.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="356.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="748.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="756.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="764.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="772.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="780.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="788.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="796.0" cy="60.0" r="1.6" fill="currentColor"/><circle cx="36.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="44.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="52.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="60.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="68.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="76.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="84.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="92.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="116.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="124.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="132.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="756.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="764.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="772.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="780.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="788.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="796.0" cy="68.0" r="1.6" fill="currentColor"/><circle cx="36.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="44.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="52.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="60.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="68.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="76.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="84.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="92.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="116.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="124.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="132.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="756.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="764.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="772.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="780.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="788.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="796.0" cy="76.0" r="1.6" fill="currentColor"/><circle cx="100.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="108.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="116.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="124.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="132.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="756.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="764.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="772.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="780.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="788.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="796.0" cy="84.0" r="1.6" fill="currentColor"/><circle cx="100.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="108.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="116.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="124.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="132.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="196.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="204.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="212.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="92.0" r="1.6" fill="currentColor"/><circle cx="132.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="196.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="204.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="212.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="100.0" r="1.6" fill="currentColor"/><circle cx="132.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="196.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="204.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="212.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="108.0" r="1.6" fill="currentColor"/><circle cx="132.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="196.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="204.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="212.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="116.0" r="1.6" fill="currentColor"/><circle cx="132.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="196.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="204.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="212.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="380.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="124.0" r="1.6" fill="currentColor"/><circle cx="140.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="196.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="204.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="212.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="380.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="132.0" r="1.6" fill="currentColor"/><circle cx="164.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="380.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="140.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="380.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="148.0" r="1.6" fill="currentColor"/><circle cx="148.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="156.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="196.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="204.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="212.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="380.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="548.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="556.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="604.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="156.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="196.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="204.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="212.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="380.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="164.0" r="1.6" fill="currentColor"/><circle cx="172.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="180.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="188.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="196.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="204.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="212.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="380.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="564.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="596.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="172.0" r="1.6" fill="currentColor"/><circle cx="204.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="212.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="380.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="524.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="532.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="540.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="572.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="588.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="180.0" r="1.6" fill="currentColor"/><circle cx="204.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="212.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="380.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="580.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="612.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="188.0" r="1.6" fill="currentColor"/><circle cx="220.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="380.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="196.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="364.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="372.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="380.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="388.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="396.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="404.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="412.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="420.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="204.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="212.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="620.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="220.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="628.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="636.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="228.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="644.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="652.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="236.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="324.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="244.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="428.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="492.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="252.0" r="1.6" fill="currentColor"/><circle cx="228.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="316.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="260.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="308.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="268.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="500.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="508.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="516.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="276.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="300.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="284.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="292.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="436.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="484.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="684.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="692.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="700.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="292.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="444.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="452.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="460.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="468.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="476.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="660.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="668.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="676.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="300.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="308.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="308.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="308.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="308.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="308.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="308.0" r="1.6" fill="currentColor"/><circle cx="284.0" cy="308.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="308.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="308.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="308.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="308.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="308.0" r="1.6" fill="currentColor"/><circle cx="796.0" cy="308.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="316.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="316.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="316.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="316.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="316.0" r="1.6" fill="currentColor"/><circle cx="276.0" cy="316.0" r="1.6" fill="currentColor"/><circle cx="708.0" cy="316.0" r="1.6" fill="currentColor"/><circle cx="716.0" cy="316.0" r="1.6" fill="currentColor"/><circle cx="724.0" cy="316.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="316.0" r="1.6" fill="currentColor"/><circle cx="740.0" cy="316.0" r="1.6" fill="currentColor"/><circle cx="796.0" cy="316.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="324.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="324.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="324.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="324.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="324.0" r="1.6" fill="currentColor"/><circle cx="732.0" cy="324.0" r="1.6" fill="currentColor"/><circle cx="780.0" cy="324.0" r="1.6" fill="currentColor"/><circle cx="788.0" cy="324.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="332.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="332.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="332.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="332.0" r="1.6" fill="currentColor"/><circle cx="268.0" cy="332.0" r="1.6" fill="currentColor"/><circle cx="780.0" cy="332.0" r="1.6" fill="currentColor"/><circle cx="788.0" cy="332.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="340.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="340.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="340.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="340.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="348.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="348.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="348.0" r="1.6" fill="currentColor"/><circle cx="260.0" cy="348.0" r="1.6" fill="currentColor"/><circle cx="236.0" cy="356.0" r="1.6" fill="currentColor"/><circle cx="244.0" cy="356.0" r="1.6" fill="currentColor"/><circle cx="252.0" cy="356.0" r="1.6" fill="currentColor"/></g> <g class="s-map-lines"> <path d="M 128 114.1 Q 263.85 18.420782851798755 399.7 78.7" fill="none" stroke="url(#s-map-6-flight-gradient)" stroke-width="1.4" stroke-dasharray="4,4" opacity="0.6"> <animate attributeName="stroke-dashoffset" values="32;0" dur="2s" repeatCount="indefinite"/> </path> <path d="M 399.7 78.7 Q 461.25 47.771395634461626 522.8 146.6" fill="none" stroke="url(#s-map-6-flight-gradient)" stroke-width="1.4" stroke-dasharray="4,4" opacity="0.6"> <animate attributeName="stroke-dashoffset" values="32;0" dur="2s" repeatCount="indefinite"/> </path> <path d="M 522.8 146.6 Q 561.8499999999999 129.39975616451906 600.9 150.2" fill="none" stroke="url(#s-map-6-flight-gradient)" stroke-width="1.4" stroke-dasharray="4,4" opacity="0.6"> <animate attributeName="stroke-dashoffset" values="32;0" dur="2s" repeatCount="indefinite"/> </path> <path d="M 600.9 150.2 Q 615.8 135.87387770539422 630.7 208.1" fill="none" stroke="url(#s-map-6-flight-gradient)" stroke-width="1.4" stroke-dasharray="4,4" opacity="0.6"> <animate attributeName="stroke-dashoffset" values="32;0" dur="2s" repeatCount="indefinite"/> </path> <path d="M 630.7 208.1 Q 683.35 177.4963472768364 736 299" fill="none" stroke="url(#s-map-6-flight-gradient)" stroke-width="1.4" stroke-dasharray="4,4" opacity="0.6"> <animate attributeName="stroke-dashoffset" values="32;0" dur="2s" repeatCount="indefinite"/> </path> <path d="M 399.7 78.7 Q 555 9.780983756295555 710.3 119.5" fill="none" stroke="url(#s-map-6-flight-gradient)" stroke-width="1.4" stroke-dasharray="4,4" opacity="0.6"> <animate attributeName="stroke-dashoffset" values="32;0" dur="2s" repeatCount="indefinite"/> </path> <path d="M 710.3 119.5 Q 419.15 -14.011508382346364 128 114.1" fill="none" stroke="url(#s-map-6-flight-gradient)" stroke-width="1.4" stroke-dasharray="4,4" opacity="0.6"> <animate attributeName="stroke-dashoffset" values="32;0" dur="2s" repeatCount="indefinite"/> </path> </g> <g class="s-map-markers"> <g class="s-map-marker" transform="translate(128, 114.1)" data-marker-index="0"> <circle cx="0" cy="0" r="3.5" fill="#FF6900"/> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.5" opacity="0.8"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.8;0" dur="1.8s" repeatCount="indefinite"/> </circle> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.2" opacity="0.6"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.6;0" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> </circle> <g class="s-map-badge" transform="translate(8, -10)"> <rect x="0" y="0" width="100.5" height="20" rx="10" fill="rgba(15,23,42,0.85)" stroke="rgba(255,255,255,0.18)" stroke-width="0.8"/> <text x="8" y="13.5" font-size="9.5" font-family="system-ui, -apple-system, sans-serif" font-weight="600" fill="#ffffff">San Francisco</text> </g> </g> <g class="s-map-marker" transform="translate(235.5, 106.5)" data-marker-index="1"> <circle cx="0" cy="0" r="3.5" fill="#FF6900"/> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.5" opacity="0.8"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.8;0" dur="1.8s" repeatCount="indefinite"/> </circle> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.2" opacity="0.6"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.6;0" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> </circle> <g class="s-map-badge" transform="translate(8, -10)"> <rect x="0" y="0" width="68" height="20" rx="10" fill="rgba(15,23,42,0.85)" stroke="rgba(255,255,255,0.18)" stroke-width="0.8"/> <text x="8" y="13.5" font-size="9.5" font-family="system-ui, -apple-system, sans-serif" font-weight="600" fill="#ffffff">New York</text> </g> </g> <g class="s-map-marker" transform="translate(399.7, 78.7)" data-marker-index="2"> <circle cx="0" cy="0" r="3.5" fill="#FF6900"/> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.5" opacity="0.8"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.8;0" dur="1.8s" repeatCount="indefinite"/> </circle> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.2" opacity="0.6"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.6;0" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> </circle> <g class="s-map-badge" transform="translate(8, -10)"> <rect x="0" y="0" width="55" height="20" rx="10" fill="rgba(15,23,42,0.85)" stroke="rgba(255,255,255,0.18)" stroke-width="0.8"/> <text x="8" y="13.5" font-size="9.5" font-family="system-ui, -apple-system, sans-serif" font-weight="600" fill="#ffffff">London</text> </g> </g> <g class="s-map-marker" transform="translate(522.8, 146.6)" data-marker-index="3"> <circle cx="0" cy="0" r="3.5" fill="#FF6900"/> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.5" opacity="0.8"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.8;0" dur="1.8s" repeatCount="indefinite"/> </circle> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.2" opacity="0.6"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.6;0" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> </circle> <g class="s-map-badge" transform="translate(8, -10)"> <rect x="0" y="0" width="48.5" height="20" rx="10" fill="rgba(15,23,42,0.85)" stroke="rgba(255,255,255,0.18)" stroke-width="0.8"/> <text x="8" y="13.5" font-size="9.5" font-family="system-ui, -apple-system, sans-serif" font-weight="600" fill="#ffffff">Dubai</text> </g> </g> <g class="s-map-marker" transform="translate(600.9, 150.2)" data-marker-index="4"> <circle cx="0" cy="0" r="3.5" fill="#FF6900"/> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.5" opacity="0.8"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.8;0" dur="1.8s" repeatCount="indefinite"/> </circle> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.2" opacity="0.6"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.6;0" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> </circle> <g class="s-map-badge" transform="translate(8, -10)"> <rect x="0" y="0" width="48.5" height="20" rx="10" fill="rgba(15,23,42,0.85)" stroke="rgba(255,255,255,0.18)" stroke-width="0.8"/> <text x="8" y="13.5" font-size="9.5" font-family="system-ui, -apple-system, sans-serif" font-weight="600" fill="#ffffff">Dhaka</text> </g> </g> <g class="s-map-marker" transform="translate(710.3, 119.5)" data-marker-index="5"> <circle cx="0" cy="0" r="3.5" fill="#FF6900"/> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.5" opacity="0.8"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.8;0" dur="1.8s" repeatCount="indefinite"/> </circle> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.2" opacity="0.6"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.6;0" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> </circle> <g class="s-map-badge" transform="translate(8, -10)"> <rect x="0" y="0" width="48.5" height="20" rx="10" fill="rgba(15,23,42,0.85)" stroke="rgba(255,255,255,0.18)" stroke-width="0.8"/> <text x="8" y="13.5" font-size="9.5" font-family="system-ui, -apple-system, sans-serif" font-weight="600" fill="#ffffff">Tokyo</text> </g> </g> <g class="s-map-marker" transform="translate(682.2, 114.7)" data-marker-index="6"> <circle cx="0" cy="0" r="3.5" fill="#FF6900"/> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.5" opacity="0.8"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.8;0" dur="1.8s" repeatCount="indefinite"/> </circle> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.2" opacity="0.6"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.6;0" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> </circle> <g class="s-map-badge" transform="translate(8, -10)"> <rect x="0" y="0" width="48.5" height="20" rx="10" fill="rgba(15,23,42,0.85)" stroke="rgba(255,255,255,0.18)" stroke-width="0.8"/> <text x="8" y="13.5" font-size="9.5" font-family="system-ui, -apple-system, sans-serif" font-weight="600" fill="#ffffff">Seoul</text> </g> </g> <g class="s-map-marker" transform="translate(630.7, 208.1)" data-marker-index="7"> <circle cx="0" cy="0" r="3.5" fill="#FF6900"/> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.5" opacity="0.8"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.8;0" dur="1.8s" repeatCount="indefinite"/> </circle> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.2" opacity="0.6"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.6;0" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> </circle> <g class="s-map-badge" transform="translate(8, -10)"> <rect x="0" y="0" width="74.5" height="20" rx="10" fill="rgba(15,23,42,0.85)" stroke="rgba(255,255,255,0.18)" stroke-width="0.8"/> <text x="8" y="13.5" font-size="9.5" font-family="system-ui, -apple-system, sans-serif" font-weight="600" fill="#ffffff">Singapore</text> </g> </g> <g class="s-map-marker" transform="translate(736, 299)" data-marker-index="8"> <circle cx="0" cy="0" r="3.5" fill="#FF6900"/> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.5" opacity="0.8"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.8;0" dur="1.8s" repeatCount="indefinite"/> </circle> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.2" opacity="0.6"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.6;0" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> </circle> <g class="s-map-badge" transform="translate(8, -10)"> <rect x="0" y="0" width="55" height="20" rx="10" fill="rgba(15,23,42,0.85)" stroke="rgba(255,255,255,0.18)" stroke-width="0.8"/> <text x="8" y="13.5" font-size="9.5" font-family="system-ui, -apple-system, sans-serif" font-weight="600" fill="#ffffff">Sydney</text> </g> </g> <g class="s-map-marker" transform="translate(296.4, 272.4)" data-marker-index="9"> <circle cx="0" cy="0" r="3.5" fill="#FF6900"/> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.5" opacity="0.8"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.8;0" dur="1.8s" repeatCount="indefinite"/> </circle> <circle cx="0" cy="0" r="3.5" fill="none" stroke="#FF6900" stroke-width="1.2" opacity="0.6"> <animate attributeName="r" values="3.5;14.700000000000001" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> <animate attributeName="opacity" values="0.6;0" dur="1.8s" begin="0.9s" repeatCount="indefinite"/> </circle> <g class="s-map-badge" transform="translate(8, -10)"> <rect x="0" y="0" width="74.5" height="20" rx="10" fill="rgba(15,23,42,0.85)" stroke="rgba(255,255,255,0.18)" stroke-width="0.8"/> <text x="8" y="13.5" font-size="9.5" font-family="system-ui, -apple-system, sans-serif" font-weight="600" fill="#ffffff">São Paulo</text> </g> </g> </g> </svg><div class="s-dotted-map-vignette" aria-hidden="true"></div></div> </div> Animated curved SVG beam connecting nodes with radiant gradient flow.
import React, { forwardRef, useRef } from "react";import { AnimatedBeam } from "@/components/sarahui/animated-beam";import { GoogleDriveIcon, NotionIcon, WhatsAppIcon, OpenAIIcon, GoogleDocsIcon, ZapierIcon, MessengerIcon } from "sarahui/icons"; const Circle = forwardRef< HTMLDivElement, { className?: string; children?: React.ReactNode }>(({ className, children }, ref) => { return ( <div ref={ref} className={`z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 p-3 shadow-md ${className || ""}`} > {children} </div> );}); Circle.displayName = "Circle"; export function AnimatedBeamDemo() { const containerRef = useRef<HTMLDivElement>(null); const div1Ref = useRef<HTMLDivElement>(null); const div2Ref = useRef<HTMLDivElement>(null); const div3Ref = useRef<HTMLDivElement>(null); const div4Ref = useRef<HTMLDivElement>(null); const div5Ref = useRef<HTMLDivElement>(null); const div6Ref = useRef<HTMLDivElement>(null); const div7Ref = useRef<HTMLDivElement>(null); return ( <div className="relative flex h-[360px] w-full items-center justify-center overflow-hidden rounded-xl border bg-background p-10 shadow-sm" ref={containerRef} > <div className="flex size-full max-h-[240px] max-w-lg flex-col items-stretch justify-between gap-10"> <div className="flex flex-row items-center justify-between"> <Circle ref={div1Ref}> <GoogleDriveIcon className="size-6" /> </Circle> <Circle ref={div5Ref}> <GoogleDocsIcon className="size-6" /> </Circle> </div> <div className="flex flex-row items-center justify-between"> <Circle ref={div2Ref}> <NotionIcon className="size-6" /> </Circle> <Circle ref={div4Ref} className="size-16"> <OpenAIIcon className="size-8" /> </Circle> <Circle ref={div6Ref}> <ZapierIcon className="size-6" /> </Circle> </div> <div className="flex flex-row items-center justify-between"> <Circle ref={div3Ref}> <WhatsAppIcon className="size-6" /> </Circle> <Circle ref={div7Ref}> <MessengerIcon className="size-6" /> </Circle> </div> </div> <AnimatedBeam containerRef={containerRef} fromRef={div1Ref} toRef={div4Ref} curvature={-75} endYOffset={-10} /> <AnimatedBeam containerRef={containerRef} fromRef={div2Ref} toRef={div4Ref} /> <AnimatedBeam containerRef={containerRef} fromRef={div3Ref} toRef={div4Ref} curvature={75} endYOffset={10} /> <AnimatedBeam containerRef={containerRef} fromRef={div5Ref} toRef={div4Ref} curvature={-75} endYOffset={-10} reverse /> <AnimatedBeam containerRef={containerRef} fromRef={div6Ref} toRef={div4Ref} reverse /> <AnimatedBeam containerRef={containerRef} fromRef={div7Ref} toRef={div4Ref} curvature={75} endYOffset={10} reverse /> </div> );}
Add AnimatedBeam to your project.
$ npx sarahui add animated-beam
import { AnimatedBeam } from 'sarahui';
<AnimatedBeam containerRef={containerRef} fromRef={fromRef} toRef={toRef} />
import React, { forwardRef, useRef } from "react";import { AnimatedBeam } from "@/components/sarahui/animated-beam";import { UserIcon, OpenAIIcon } from "sarahui/icons"; export function AnimatedBeamUniDirectionalDemo() { const containerRef = useRef<HTMLDivElement>(null); const fromRef = useRef<HTMLDivElement>(null); const toRef = useRef<HTMLDivElement>(null); return ( <div className="relative flex h-[340px] w-full items-center justify-center overflow-hidden rounded-xl border bg-background p-10" ref={containerRef} > <div className="flex w-full max-w-sm items-center justify-between"> <div ref={fromRef} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <UserIcon className="size-6" /> </div> <div ref={toRef} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <OpenAIIcon className="size-6" /> </div> </div> <AnimatedBeam containerRef={containerRef} fromRef={fromRef} toRef={toRef} duration={3} /> </div> );}
import React, { useRef } from "react";import { AnimatedBeam } from "@/components/sarahui/animated-beam";import { UserIcon, OpenAIIcon } from "sarahui/icons"; export function AnimatedBeamBiDirectionalDemo() { const containerRef = useRef<HTMLDivElement>(null); const div1Ref = useRef<HTMLDivElement>(null); const div2Ref = useRef<HTMLDivElement>(null); return ( <div className="relative flex h-[340px] w-full items-center justify-center overflow-hidden rounded-xl border bg-background p-10" ref={containerRef} > <div className="flex w-full max-w-sm items-center justify-between"> <div ref={div1Ref} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <UserIcon className="size-6" /> </div> <div ref={div2Ref} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <OpenAIIcon className="size-6" /> </div> </div> <AnimatedBeam containerRef={containerRef} fromRef={div1Ref} toRef={div2Ref} curvature={50} duration={3} /> <AnimatedBeam containerRef={containerRef} fromRef={div2Ref} toRef={div1Ref} curvature={-50} duration={3} reverse /> </div> );}
import React, { useRef } from "react";import { AnimatedBeam } from "@/components/sarahui/animated-beam";import { GoogleDriveIcon, GoogleDocsIcon, WhatsAppIcon, MessengerIcon, NotionIcon, OpenAIIcon, UserIcon } from "sarahui/icons"; export function AnimatedBeamMultipleInputsDemo() { const containerRef = useRef<HTMLDivElement>(null); const div1Ref = useRef<HTMLDivElement>(null); const div2Ref = useRef<HTMLDivElement>(null); const div3Ref = useRef<HTMLDivElement>(null); const div4Ref = useRef<HTMLDivElement>(null); const div5Ref = useRef<HTMLDivElement>(null); const hubRef = useRef<HTMLDivElement>(null); const outRef = useRef<HTMLDivElement>(null); return ( <div className="relative flex h-[500px] w-full items-center justify-center overflow-hidden rounded-xl border bg-background p-10" ref={containerRef} > <div className="flex size-full max-w-lg flex-row items-stretch justify-between gap-10"> <div className="flex flex-col justify-center gap-2"> <div ref={div1Ref} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <GoogleDriveIcon className="size-5" /> </div> <div ref={div2Ref} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <GoogleDocsIcon className="size-5" /> </div> <div ref={div3Ref} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <WhatsAppIcon className="size-5" /> </div> <div ref={div4Ref} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <MessengerIcon className="size-5" /> </div> <div ref={div5Ref} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <NotionIcon className="size-5" /> </div> </div> <div className="flex flex-col justify-center"> <div ref={hubRef} className="z-10 flex size-16 items-center justify-center rounded-full border-2 border-primary bg-white dark:bg-zinc-900 shadow-lg"> <OpenAIIcon className="size-8" /> </div> </div> <div className="flex flex-col justify-center"> <div ref={outRef} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <UserIcon className="size-6" /> </div> </div> </div> <AnimatedBeam containerRef={containerRef} fromRef={div1Ref} toRef={hubRef} /> <AnimatedBeam containerRef={containerRef} fromRef={div2Ref} toRef={hubRef} /> <AnimatedBeam containerRef={containerRef} fromRef={div3Ref} toRef={hubRef} /> <AnimatedBeam containerRef={containerRef} fromRef={div4Ref} toRef={hubRef} /> <AnimatedBeam containerRef={containerRef} fromRef={div5Ref} toRef={hubRef} /> <AnimatedBeam containerRef={containerRef} fromRef={hubRef} toRef={outRef} /> </div> );}
import React, { useRef } from "react";import { AnimatedBeam } from "@/components/sarahui/animated-beam";import { GoogleDriveIcon, GoogleDocsIcon, WhatsAppIcon, MessengerIcon, NotionIcon, OpenAIIcon, UserIcon } from "sarahui/icons"; export function AnimatedBeamMultipleOutputsDemo() { const containerRef = useRef<HTMLDivElement>(null); const div1Ref = useRef<HTMLDivElement>(null); const div2Ref = useRef<HTMLDivElement>(null); const div3Ref = useRef<HTMLDivElement>(null); const div4Ref = useRef<HTMLDivElement>(null); const div5Ref = useRef<HTMLDivElement>(null); const hubRef = useRef<HTMLDivElement>(null); const userRef = useRef<HTMLDivElement>(null); return ( <div className="relative flex h-[500px] w-full items-center justify-center overflow-hidden rounded-xl border bg-background p-10" ref={containerRef} > <div className="flex size-full max-w-lg flex-row items-stretch justify-between gap-10"> <div className="flex flex-col justify-center"> <div ref={userRef} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <UserIcon className="size-6" /> </div> </div> <div className="flex flex-col justify-center"> <div ref={hubRef} className="z-10 flex size-16 items-center justify-center rounded-full border-2 border-primary bg-white dark:bg-zinc-900 shadow-lg"> <OpenAIIcon className="size-8" /> </div> </div> <div className="flex flex-col justify-center gap-2"> <div ref={div1Ref} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <GoogleDriveIcon className="size-5" /> </div> <div ref={div2Ref} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <GoogleDocsIcon className="size-5" /> </div> <div ref={div3Ref} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <WhatsAppIcon className="size-5" /> </div> <div ref={div4Ref} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <MessengerIcon className="size-5" /> </div> <div ref={div5Ref} className="z-10 flex size-12 items-center justify-center rounded-full border-2 bg-white dark:bg-zinc-900 shadow-md"> <NotionIcon className="size-5" /> </div> </div> </div> <AnimatedBeam containerRef={containerRef} fromRef={div1Ref} toRef={hubRef} duration={3} /> <AnimatedBeam containerRef={containerRef} fromRef={div2Ref} toRef={hubRef} duration={3} /> <AnimatedBeam containerRef={containerRef} fromRef={div3Ref} toRef={hubRef} duration={3} /> <AnimatedBeam containerRef={containerRef} fromRef={div4Ref} toRef={hubRef} duration={3} /> <AnimatedBeam containerRef={containerRef} fromRef={div5Ref} toRef={hubRef} duration={3} /> <AnimatedBeam containerRef={containerRef} fromRef={hubRef} toRef={userRef} duration={3} /> </div> );}
Configuration parameters and properties for AnimatedBeam.
| Prop | Type | Default | Description |
|---|---|---|---|
curvature |
number | 0 |
The curvature arch of the quadratic Bezier beam curve |
duration |
number | 5 |
Duration in seconds of one complete beam animation cycle |
delay |
number | 0 |
Initial animation delay in seconds |
reverse |
boolean | false |
Whether radiant beam travel direction is reversed |
pathColor |
string | 'gray' |
Stroke color for the background track line |
pathWidth |
number | 2 |
Stroke width thickness of the beam line |
pathOpacity |
number | 0.2 |
Opacity level of the static background track path |
gradientStartColor |
string | '#ffaa40' |
Leading start color of glowing beam gradient |
gradientStopColor |
string | '#9c40ff' |
Trailing stop color of glowing beam gradient |
startX |
number | 0 |
Horizontal start coordinate for custom path |
startY |
number | 0 |
Vertical start coordinate for custom path |
endX |
number | 0 |
Horizontal end coordinate for custom path |
endY |
number | 0 |
Vertical end coordinate for custom path |
d |
string | '' |
Explicit SVG path definition string |
className |
string | '' |
Optional CSS class for SVG beam element |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for SVG element |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.AnimatedBeam({ duration: 3 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="animated-beam-demo-container" data-s-beam-container style="position:relative;display:flex;height:300px;width:100%;align-items:center;justify-content:center;overflow:hidden;border-radius:12px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);padding:40px;"> <div style="display:flex;width:100%;max-width:512px;height:100%;max-height:200px;flex-direction:row;align-items:stretch;justify-content:space-between;position:relative;z-index:2;"> <!-- Left Column (Inputs) --> <div style="display:flex;flex-direction:column;justify-content:space-between;align-items:center;"> <div id="demo-animatedbeam-drive" class="beam-circle-node" title="Google Drive"><svg class="s-icon" width="24" height="24" viewBox="0 0 87.3 78" aria-hidden="true"><g><path d="m6.6 66.85 3.85 6.65c.8 1.4 1.95 2.5 3.3 3.3l13.75-23.8h-27.5c0 1.55.4 3.1 1.2 4.5z" fill="#0066da"/><path d="m43.65 25-13.75-23.8c-1.35.8-2.5 1.9-3.3 3.3l-25.4 44a9.06 9.06 0 0 0 -1.2 4.5h27.5z" fill="#00ac47"/><path d="m73.55 76.8c1.35-.8 2.5-1.9 3.3-3.3l1.6-2.75 7.65-13.25c.8-1.4 1.2-2.95 1.2-4.5h-27.502l5.852 11.5z" fill="#ea4335"/><path d="m43.65 25 13.75-23.8c-1.35-.8-2.9-1.2-4.5-1.2h-18.5c-1.6 0-3.15.45-4.5 1.2z" fill="#00832d"/><path d="m59.8 53h-32.3l-13.75 23.8c1.35.8 2.9 1.2 4.5 1.2h50.8c1.6 0 3.15-.45 4.5-1.2z" fill="#2684fc"/><path d="m73.4 26.5-12.7-22c-.8-1.4-1.95-2.5-3.3-3.3l-13.75 23.8 16.15 28h27.45c0-1.55-.4-3.1-1.2-4.5z" fill="#ffba00"/></g></svg></div> <div id="demo-animatedbeam-notion" class="beam-circle-node" title="Notion"><svg class="s-icon" width="24" height="24" viewBox="0 0 100 100" aria-hidden="true"><g><path d="M6.017 4.313l55.333 -4.087c6.797 -0.583 8.543 -0.19 12.817 2.917l17.663 12.443c2.913 2.14 3.883 2.723 3.883 5.053v68.243c0 4.277 -1.553 6.807 -6.99 7.193L24.467 99.967c-4.08 0.193 -6.023 -0.39 -8.16 -3.113L3.3 79.94c-2.333 -3.113 -3.3 -5.443 -3.3 -8.167V11.113c0 -3.497 1.553 -6.413 6.017 -6.8z" fill="#ffffff"/><path d="M61.35 0.227l-55.333 4.087C1.553 4.7 0 7.617 0 11.113v60.66c0 2.723 0.967 5.053 3.3 8.167l13.007 16.913c2.137 2.723 4.08 3.307 8.16 3.113l64.257 -3.89c5.433 -0.387 6.99 -2.917 6.99 -7.193V20.64c0 -2.21 -0.873 -2.847 -3.443 -4.733L74.167 3.143c-4.273 -3.107 -6.02 -3.5 -12.817 -2.917zM25.92 19.523c-5.247 0.353 -6.437 0.433 -9.417 -1.99L8.927 11.507c-0.77 -0.78 -0.383 -1.753 1.557 -1.947l53.193 -3.887c4.467 -0.39 6.793 1.167 8.54 2.527l9.123 6.61c0.39 0.197 1.36 1.36 0.193 1.36l-54.933 3.307 -0.68 0.047zM19.803 88.3V30.367c0 -2.53 0.777 -3.697 3.103 -3.893L86 22.78c2.14 -0.193 3.107 1.167 3.107 3.693v57.547c0 2.53 -0.39 4.67 -3.883 4.863l-60.377 3.5c-3.493 0.193 -5.043 -0.97 -5.043 -4.083zm59.6 -54.827c0.387 1.75 0 3.5 -1.75 3.7l-2.91 0.577v42.773c-2.527 1.36 -4.853 2.137 -6.797 2.137 -3.107 0 -3.883 -0.973 -6.21 -3.887l-19.03 -29.94v28.967l6.02 1.363s0 3.5 -4.857 3.5l-13.39 0.777c-0.39 -0.78 0 -2.723 1.357 -3.11l3.497 -0.97v-38.3L30.48 40.667c-0.39 -1.75 0.58 -4.277 3.3 -4.473l14.367 -0.967 19.8 30.327v-26.83l-5.047 -0.58c-0.39 -2.143 1.163 -3.7 3.103 -3.89l13.4 -0.78z" fill="#000000" fill-rule="evenodd" clip-rule="evenodd"/></g></svg></div> <div id="demo-animatedbeam-whatsapp" class="beam-circle-node" title="WhatsApp"><svg class="s-icon" width="24" height="24" viewBox="0 0 175.216 175.552" aria-hidden="true"><g><path d="m12.966 161.238 10.439-38.114a73.42 73.42 0 0 1-9.821-36.772c.017-40.556 33.021-73.55 73.578-73.55 19.681.01 38.154 7.669 52.047 21.572s21.537 32.383 21.53 52.037c-.018 40.553-33.027 73.553-73.578 73.553h-.032c-12.313-.005-24.412-3.094-35.159-8.954z" fill="#25D366"/><path d="M87.184 25.227c-33.733 0-61.166 27.423-61.178 61.13a60.98 60.98 0 0 0 9.349 32.535l1.455 2.312-6.179 22.559 23.146-6.069 2.235 1.324c9.387 5.571 20.15 8.518 31.126 8.524h.023c33.707 0 61.14-27.426 61.153-61.135a60.75 60.75 0 0 0-17.895-43.251 60.75 60.75 0 0 0-43.235-17.929z" fill="#25D366"/><path d="M68.772 55.603c-1.378-3.061-2.828-3.123-4.137-3.176l-3.524-.043c-1.226 0-3.218.46-4.902 2.3s-6.435 6.287-6.435 15.332 6.588 17.785 7.506 19.013 12.718 20.381 31.405 27.75c15.529 6.124 18.689 4.906 22.061 4.6s10.877-4.447 12.408-8.74 1.532-7.971 1.073-8.74-1.685-1.226-3.525-2.146-10.877-5.367-12.562-5.981-2.91-.919-4.137.921-4.746 5.979-5.819 7.206-2.144 1.381-3.984.462-7.76-2.861-14.784-9.124c-5.465-4.873-9.154-10.891-10.228-12.73s-.114-2.835.808-3.751c.825-.824 1.838-2.147 2.759-3.22s1.224-1.84 1.836-3.065.307-2.301-.153-3.22-4.032-10.011-5.666-13.647" fill="#ffffff" fill-rule="evenodd"/></g></svg></div> </div> <!-- Center Column (AI Hub) --> <div style="display:flex;flex-direction:column;justify-content:center;align-items:center;"> <div id="demo-animatedbeam-openai" class="beam-circle-node beam-circle-hub" title="OpenAI"><svg class="s-icon" width="32" height="32" viewBox="0 0 24 24" aria-hidden="true"><path fill="currentColor" d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg></div> </div> <!-- Right Column (Outputs) --> <div style="display:flex;flex-direction:column;justify-content:space-between;align-items:center;"> <div id="demo-animatedbeam-docs" class="beam-circle-node" title="Google Docs"><svg class="s-icon" width="24" height="24" viewBox="0 0 47 65" aria-hidden="true"><g fill="none" fill-rule="evenodd"><path d="M29.375 0H4.406C1.983 0 0 1.994 0 4.432v56.136C0 63.006 1.983 65 4.406 65h38.188C45.017 65 47 63.006 47 60.568V17.727L29.375 0z" fill="#4285F4"/><path d="M29.375 0v13.295c0 2.449 1.972 4.432 4.406 4.432H47L29.375 0z" fill="#A1C2FA"/><path d="M11.75 47.273h23.5v-2.955h-23.5v2.955zm0 5.909h17.625v-2.955H11.75v2.955zm0-20.682v2.955h23.5V32.5h-23.5zm0 8.864h23.5v-2.955h-23.5v2.955z" fill="#F1F1F1"/></g></svg></div> <div id="demo-animatedbeam-zapier" class="beam-circle-node" title="Zapier"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" aria-hidden="true"><path d="M13.5 2L3 14.5h7.5L9.5 22 21 9.5h-7.5L13.5 2z" fill="#FF4F00"/></svg></div> <div id="demo-animatedbeam-messenger" class="beam-circle-node" title="Messenger"><svg class="s-icon" width="24" height="24" viewBox="0 0 48 48" aria-hidden="true"><g><path d="M24 4C12.95 4 4 12.73 4 23.5c0 5.61 2.425 10.665 6.319 14.224v4.025c0 1.264 1.33 2.086 2.46 1.521l4.425-2.206c2.09.507 4.31.786 6.796.786 11.05 0 20-8.73 20-19.5S35.05 4 24 4z" fill="#0084FF"/><path d="M34.394 18.501l-5.7 4.22c-.61.46-1.44.46-2.04.01L22.68 19.74c-1.68-1.25-4.06-.82-5.19.94l-1.21 1.89-4.11 6.68c-.6.94.55 2.01 1.44 1.34l5.7-4.22c.61-.46 1.44-.46 2.04-.01l3.974 2.991c1.68 1.25 4.06.82 5.19-.94l1.21-1.89 4.11-6.68c.44-.68-.71-1.75-1.44-1.34z" fill="#ffffff"/></g></svg></div> </div> </div> <svg class="beam-svg-overlay" style="position:absolute;inset:0;width:100%;height:100%;pointer-events:none;z-index:1;" aria-hidden="true"> <defs> <linearGradient id="demo-animatedbeam-grad-drive" gradientUnits="userSpaceOnUse"> <stop offset="0%" stop-color="#ffaa40" stop-opacity="0"></stop> <stop offset="0%" stop-color="#ffaa40"></stop> <stop offset="32.5%" stop-color="#9c40ff"></stop> <stop offset="100%" stop-color="#9c40ff" stop-opacity="0"></stop> </linearGradient> <linearGradient id="demo-animatedbeam-grad-notion" gradientUnits="userSpaceOnUse"> <stop offset="0%" stop-color="#ffaa40" stop-opacity="0"></stop> <stop offset="0%" stop-color="#ffaa40"></stop> <stop offset="32.5%" stop-color="#9c40ff"></stop> <stop offset="100%" stop-color="#9c40ff" stop-opacity="0"></stop> </linearGradient> <linearGradient id="demo-animatedbeam-grad-whatsapp" gradientUnits="userSpaceOnUse"> <stop offset="0%" stop-color="#ffaa40" stop-opacity="0"></stop> <stop offset="0%" stop-color="#ffaa40"></stop> <stop offset="32.5%" stop-color="#9c40ff"></stop> <stop offset="100%" stop-color="#9c40ff" stop-opacity="0"></stop> </linearGradient> <linearGradient id="demo-animatedbeam-grad-docs" gradientUnits="userSpaceOnUse"> <stop offset="0%" stop-color="#ffaa40" stop-opacity="0"></stop> <stop offset="0%" stop-color="#ffaa40"></stop> <stop offset="32.5%" stop-color="#9c40ff"></stop> <stop offset="100%" stop-color="#9c40ff" stop-opacity="0"></stop> </linearGradient> <linearGradient id="demo-animatedbeam-grad-zapier" gradientUnits="userSpaceOnUse"> <stop offset="0%" stop-color="#ffaa40" stop-opacity="0"></stop> <stop offset="0%" stop-color="#ffaa40"></stop> <stop offset="32.5%" stop-color="#9c40ff"></stop> <stop offset="100%" stop-color="#9c40ff" stop-opacity="0"></stop> </linearGradient> <linearGradient id="demo-animatedbeam-grad-messenger" gradientUnits="userSpaceOnUse"> <stop offset="0%" stop-color="#ffaa40" stop-opacity="0"></stop> <stop offset="0%" stop-color="#ffaa40"></stop> <stop offset="32.5%" stop-color="#9c40ff"></stop> <stop offset="100%" stop-color="#9c40ff" stop-opacity="0"></stop> </linearGradient> </defs> <!-- Background Static Paths --> <path class="s-beam-path-bg" data-bg-for="demo-animatedbeam-p-drive" /> <path class="s-beam-path-bg" data-bg-for="demo-animatedbeam-p-notion" /> <path class="s-beam-path-bg" data-bg-for="demo-animatedbeam-p-whatsapp" /> <path class="s-beam-path-bg" data-bg-for="demo-animatedbeam-p-docs" /> <path class="s-beam-path-bg" data-bg-for="demo-animatedbeam-p-zapier" /> <path class="s-beam-path-bg" data-bg-for="demo-animatedbeam-p-messenger" /> <!-- Animated Beam Flow Paths (Native SarahUI specification) --> <path class="s-beam-path" id="demo-animatedbeam-p-drive" data-from="#demo-animatedbeam-drive" data-to="#demo-animatedbeam-openai" data-curvature="-75" data-end-y-offset="-10" stroke="url(#demo-animatedbeam-grad-drive)" /> <path class="s-beam-path" id="demo-animatedbeam-p-notion" data-from="#demo-animatedbeam-notion" data-to="#demo-animatedbeam-openai" stroke="url(#demo-animatedbeam-grad-notion)" /> <path class="s-beam-path" id="demo-animatedbeam-p-whatsapp" data-from="#demo-animatedbeam-whatsapp" data-to="#demo-animatedbeam-openai" data-curvature="75" data-end-y-offset="10" stroke="url(#demo-animatedbeam-grad-whatsapp)" /> <path class="s-beam-path s-beam-path--reverse" id="demo-animatedbeam-p-docs" data-from="#demo-animatedbeam-docs" data-to="#demo-animatedbeam-openai" data-curvature="-75" data-end-y-offset="-10" data-reverse="true" stroke="url(#demo-animatedbeam-grad-docs)" /> <path class="s-beam-path s-beam-path--reverse" id="demo-animatedbeam-p-zapier" data-from="#demo-animatedbeam-zapier" data-to="#demo-animatedbeam-openai" data-reverse="true" stroke="url(#demo-animatedbeam-grad-zapier)" /> <path class="s-beam-path s-beam-path--reverse" id="demo-animatedbeam-p-messenger" data-from="#demo-animatedbeam-messenger" data-to="#demo-animatedbeam-openai" data-curvature="75" data-end-y-offset="10" data-reverse="true" stroke="url(#demo-animatedbeam-grad-messenger)" /> </svg> </div>Luminous glowing gradient beam smoothly tracing around container borders.
Enter your credentials to access your account.
import { BorderBeam } from "@/components/sarahui/border-beam"; export function BorderBeamDemo() { return ( <div className="relative flex h-[200px] w-full max-w-sm flex-col items-center justify-center overflow-hidden rounded-xl border bg-background md:shadow-xl"> <span className="pointer-events-none whitespace-pre-wrap bg-gradient-to-b from-black to-gray-300 bg-clip-text text-center text-4xl font-semibold leading-none text-transparent dark:from-white dark:to-slate-900/10"> Border Beam </span> <BorderBeam size={250} duration={12} delay={9} /> </div> );}
Add BorderBeam to your project.
$ npx sarahui add border-beam
import { BorderBeam } from 'sarahui';
<div className="relative h-[500px] w-full overflow-hidden"> <BorderBeam /></div>
Stairway to Heaven - Led Zeppelin
import { Play, SkipBack, SkipForward } from "lucide-react";import { Button } from "@/components/ui/button";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { BorderBeam } from "@/components/sarahui/border-beam"; export function BorderBeamDual() { return ( <Card className="relative w-[350px] overflow-hidden"> <CardHeader> <CardTitle>Now Playing</CardTitle> <CardDescription>Stairway to Heaven - Led Zeppelin</CardDescription> </CardHeader> <CardContent> <div className="flex flex-col items-center gap-4"> <div className="h-48 w-48 rounded-lg bg-gradient-to-br from-purple-500 to-pink-500" /> <div className="bg-secondary h-1 w-full rounded-full"> <div className="bg-primary h-full w-1/3 rounded-full" /> </div> <div className="text-muted-foreground flex w-full justify-between text-sm"> <span>2:45</span> <span>8:02</span> </div> </div> </CardContent> <CardFooter className="flex justify-center gap-4"> <Button variant="outline" size="icon" className="rounded-full"> <SkipBack className="size-4" /> </Button> <Button size="icon" className="rounded-full"> <Play className="size-4" /> </Button> <Button variant="outline" size="icon" className="rounded-full"> <SkipForward className="size-4" /> </Button> </CardFooter> <BorderBeam duration={6} size={400} className="from-transparent via-red-500 to-transparent" /> <BorderBeam duration={6} delay={3} size={400} borderWidth={2} className="from-transparent via-blue-500 to-transparent" /> </Card> );}
Enter your credentials to access your account.
import { Button } from "@/components/ui/button";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/ui/card";import { Input } from "@/components/ui/input";import { Label } from "@/components/ui/label";import { BorderBeam } from "@/components/sarahui/border-beam"; export function BorderBeamReverse() { return ( <Card className="relative w-[350px] overflow-hidden"> <CardHeader> <CardTitle>Login</CardTitle> <CardDescription> Enter your credentials to access your account. </CardDescription> </CardHeader> <CardContent> <form> <div className="grid w-full items-center gap-4"> <div className="flex flex-col space-y-1.5"> <Label htmlFor="email">Email</Label> <Input id="email" type="email" placeholder="Enter your email" /> </div> <div className="flex flex-col space-y-1.5"> <Label htmlFor="password">Password</Label> <Input id="password" type="password" placeholder="Enter your password" /> </div> </div> </form> </CardContent> <CardFooter className="flex justify-between"> <Button variant="outline">Register</Button> <Button>Login</Button> </CardFooter> <BorderBeam duration={4} size={300} reverse className="from-transparent via-green-500 to-transparent" /> </Card> );}
import { Button } from "@/components/ui/button";import { BorderBeam } from "@/components/sarahui/border-beam"; export function BorderBeamButton() { return ( <Button className="relative overflow-hidden" size="lg" variant="outline"> Buy Now <BorderBeam size={40} initialOffset={20} className="from-transparent via-yellow-500 to-transparent" transition={{ type: "spring", stiffness: 60, damping: 20, }} /> </Button> );}
Configuration parameters and properties for BorderBeam.
| Prop | Type | Default | Description |
|---|---|---|---|
size |
number | string | 200 |
Size (in px) of perimeter glowing laser beam |
duration |
number | string | 15 |
Duration (in seconds) of one complete border cycle |
delay |
number | string | 0 |
Initial animation start delay in seconds |
colorFrom |
string | '#ffaa40' |
Leading gradient color of the border beam |
colorTo |
string | '#9c40ff' |
Trailing gradient color of the border beam |
reverse |
boolean | false |
Whether border beam animates counter-clockwise |
initialOffset |
number | string | 0 |
Initial perimeter offset percentage along border path |
borderWidth |
number | string | 1.5 |
Thickness of the glowing border stroke in pixels |
className |
string | '' |
Optional CSS class for border beam container |
class |
string | '' |
Alternative class name property |
style |
string | '' |
Inline style string for custom CSS properties |
attrs |
Record<string, any> | {} |
Additional HTML attributes for beam container |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.html`<div style="position:relative;padding:24px;border:1px solid var(--s-border-default);">${UI.BorderBeam({ size: 160 })}<span>Animated Border</span></div>`; document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="demo-login-card"> <div style="margin-bottom:20px;"> <h3>Login</h3> <p>Enter your credentials to access your account.</p> </div> <form onsubmit="return false;"> <div class="demo-login-field"> <label>Email</label> <input type="email" placeholder="Enter your email" /> </div> <div class="demo-login-field"> <label>Password</label> <input type="password" placeholder="Enter your password" /> </div> <div class="demo-login-actions"> <button type="button" class="demo-login-btn-outline">Register</button> <button type="button" class="demo-login-btn-primary">Login</button> </div> </form> <div class="s-border-beam" style="--border-beam-width:1px" aria-hidden="true"><div class="s-border-beam-glow" style="width:100px;height:100px;offset-path:rect(0 auto auto 0 round 100px);--initial-offset:0%;--duration:8s;--delay:0s;--color-from:#ffaa40;--color-to:#9c40ff;background:linear-gradient(to left, #ffaa40, #9c40ff, transparent);animation-duration:8s"></div></div> </div> Animated multi-color gradient rim highlighting card borders.
Enter your credentials to access your account
import { ShineBorder } from "@/components/sarahui/shine-border"; export function ShineBorderDemo() { return ( <ShineBorder className="relative flex h-[200px] w-full max-w-sm flex-col items-center justify-center overflow-hidden rounded-lg border bg-background md:shadow-xl" color={["#A07CFE", "#FE8FB5", "#FFBE7B"]} > <span className="pointer-events-none whitespace-pre-wrap bg-gradient-to-b from-black to-gray-300 bg-clip-text text-center text-4xl font-semibold leading-none text-transparent dark:from-white dark:to-slate-900/10"> Shine Border </span> </ShineBorder> );}
Add ShineBorder to your project.
$ npx sarahui add shine-border
import { ShineBorder } from 'sarahui';
<div className="relative h-[500px] w-full overflow-hidden"> <ShineBorder /></div>
Enter your credentials to access your account
import { ShineBorder } from "@/components/sarahui/shine-border"; export function ShineBorderDemo() { return ( <ShineBorder borderWidth={1} duration={6}> <div className="p-4">Content</div> </ShineBorder> );}
import { ShineBorder } from "@/components/sarahui/shine-border"; export function ShineBorderMonotone() { return ( <ShineBorder color={["#A1A1AA"]} duration={8}> <div className="p-4">Monotone Shine</div> </ShineBorder> );}
Configuration parameters and properties for ShineBorder.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
any | - |
Card content or elements wrapped inside shining border |
borderWidth |
number | 1 |
Border stroke width in pixels |
duration |
number | 14 |
Duration in seconds for one full shine rotation |
color |
string | string[] | '#000000' |
Primary shine highlight color or array of colors |
shineColor |
string | string[] | '' |
Alternative shine color property |
className |
string | '' |
Optional CSS class for shine container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for container |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.ShineBorder({ children: "Card with animated dynamic shining border" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="demo-login-card"> <div class="s-shine-border" style="border-width:1px;--border-width:1px;--shine-duration:14s;--duration:14s;--shine-color:#A07CFE,#FE8FB5,#FFBE7B;"><div class="s-shine-border-inner"></div></div> <div style="margin-bottom:20px;"> <h3>Login</h3> <p>Enter your credentials to access your account</p> </div> <form onsubmit="return false;"> <div class="demo-login-field"> <label>Email</label> <input type="email" placeholder="name@example.com" /> </div> <div class="demo-login-field"> <label>Password</label> <input type="password" /> </div> <div style="margin-top:20px;"> <button type="button" class="demo-login-btn-primary-full">Sign In</button> </div> </form> </div> Spotlight radial gradient following cursor movements across the surface.
Enter your credentials to access your account
import { MagicCard } from "@/components/sarahui/magic-card"; export function MagicCardDemo() { return ( <div className="flex h-[500px] w-full flex-col gap-4 lg:h-[250px] lg:flex-row"> <MagicCard className="cursor-pointer flex-col items-center justify-center text-4xl shadow-2xl" gradientColor="#262626" > Magic Card </MagicCard> <MagicCard className="cursor-pointer flex-col items-center justify-center text-4xl shadow-2xl" gradientColor="#262626" > Spotlight Effect </MagicCard> </div> );}
Add MagicCard to your project.
$ npx sarahui add magic-card
import { MagicCard } from 'sarahui';
<MagicCard> <div className="p-4"> <p>Hello World</p> <span>Hover me</span> </div></MagicCard>
Creator of sarahUI
Design Engineer focused on Thoughtful UI & Performance
Passionate about thoughtful design systems, fluid web animations, and zero-dependency modern UI.
import { useTheme } from "next-themes";import { Button } from "@/components/sarahui/button";import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle,} from "@/components/sarahui/card";import { AvatarCircles } from "@/components/sarahui/avatar-circles";import { MagicCard } from "sarahui"; export function MagicCardOrbDemo() { const { theme } = useTheme(); const isDark = theme === "dark"; return ( <Card className="w-full max-w-sm border-none p-0 shadow-none"> <MagicCard mode="orb" glowFrom={isDark ? "#ee4f27" : "#E9D5FF"} glowTo={isDark ? "#6b21ef" : "#FBCFE8"} className="p-0" > <CardHeader className="border-border border-b p-4 [.border-b]:pb-4"> <div className="flex items-center gap-3"> <AvatarCircles avatarUrls={[ { imageUrl: "assets/avatar-3.svg", profileUrl: "https://github.com/masarax", }, ]} /> <div className="flex-1"> <CardTitle>MASARA X</CardTitle> <CardDescription className="mt-1"> Creator of sarahUI </CardDescription> </div> </div> </CardHeader> <CardContent className="space-y-2 p-4"> <p className="text-sm font-medium"> Design Engineer focused on Thoughtful UI & Performance </p> <p className="text-muted-foreground text-sm leading-relaxed"> Passionate about thoughtful design systems, fluid web animations, and zero-dependency modern UI. </p> </CardContent> <CardFooter className="border-border border-t p-4 [.border-t]:pt-4"> <Button asChild className="w-full"> <a href="https://github.com/masarax" target="_blank" rel="noopener noreferrer" className="flex items-center justify-center gap-2" > View on GitHub </a> </Button> </CardFooter> </MagicCard> </Card> );}
Configuration parameters and properties for MagicCard.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
any | - |
Card content and body elements |
mode |
'gradient' | 'orb' | 'gradient' |
Card illumination mode (subtle spotlight gradient or glowing orb) |
gradientSize |
number | 200 |
Radial spotlight radius size in pixels |
gradientColor |
string | '#262626' |
Color of the interactive hover gradient spotlight |
gradientOpacity |
number | 0.8 |
Maximum opacity of spotlight on cursor hover |
gradientFrom |
string | '' |
Leading color stop for gradient glow |
gradientTo |
string | '' |
Trailing color stop for gradient glow |
glowFrom |
string | '' |
Alternative leading glow color |
glowTo |
string | '' |
Alternative trailing glow color |
glowAngle |
number | 0 |
Rotation angle for directional glow |
glowSize |
number | 200 |
Radius size for glowing backdrop |
glowBlur |
number | 40 |
Filter blur radius for glow effect |
glowOpacity |
number | 0.8 |
Backdrop glow layer opacity |
className |
string | '' |
Optional CSS class for card article container |
class |
string | '' |
Alternative class name property |
style |
string | '' |
Inline style string |
attrs |
Record<string, any> | {} |
Additional HTML attributes for card element |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.MagicCard({ children: "Hover over this card to see the dynamic radial gradient follow your cursor." }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;justify-content:center;align-items:center;width:100%;padding:16px 0;"> <div style="width:100%;max-width:380px;"> <div class="s-magic-card magic-card-login-demo" data-s-magic-card="true" data-mode="gradient" data-gradient-size="200" data-glow-size="340" style="--gradient-size:200px;--magic-color:rgba(255,255,255,0.08);--gradient-opacity:0.8;--gradient-from:#9E7AFF;--gradient-to:#FE8BBB;--glow-from:#ee4f27;--glow-to:#6b21ef;--glow-angle:90deg;--glow-size:340px;--glow-blur:65px;--glow-opacity:0.38"><div class="s-magic-card-bg" aria-hidden="true"></div><div class="s-magic-card-spotlight" aria-hidden="true"></div><div class="s-magic-card-content"> <div style="border-bottom:1px solid var(--s-border-default);padding:20px 24px;"> <h3 style="font-size:18px;font-weight:700;color:var(--s-text-primary);margin:0 0 4px 0;">Login</h3> <p class="s-small s-muted" style="margin:0;">Enter your credentials to access your account</p> </div> <div style="padding:20px 24px;display:flex;flex-direction:column;gap:14px;"> <div style="display:flex;flex-direction:column;gap:6px;"> <label for="magic-card-email" style="font-size:13px;font-weight:600;color:var(--s-text-primary);">Email</label> <input id="magic-card-email" type="email" placeholder="name@example.com" class="s-input" style="width:100%;box-sizing:border-box;" /> </div> <div style="display:flex;flex-direction:column;gap:6px;"> <label for="magic-card-password" style="font-size:13px;font-weight:600;color:var(--s-text-primary);">Password</label> <input id="magic-card-password" type="password" placeholder="••••••••" class="s-input" style="width:100%;box-sizing:border-box;" /> </div> </div> <div style="border-top:1px solid var(--s-border-default);padding:16px 24px;"> <button type="button" class="s-button s-button--primary" style="width:100%;justify-content:center;"> <span>Sign In</span> </button> </div> </div></div> </div> </div> A diagonal light glare on hover using a pseudo-element gradient, CSS variables, and smooth transition.
For teams that need more.
import { GlareHover } from "@/components/sarahui/glare-hover";import { Card } from "@/components/sarahui/card"; export function GlareHoverDemo() { return ( <div className="flex items-center justify-center p-6"> <GlareHover className="rounded-xl"> <Card title="Specular Glare Card" description="Hover over this card to watch the 3D glare sheen sweep across the surface." /> </GlareHover> </div> );}
Add GlareHover to your project.
$ npx sarahui add glare-hover
import { GlareHover } from 'sarahui';
<GlareHover className="rounded-lg"> <Card> <CardContent> <CardTitle>Hello World</CardTitle> <CardDescription>Hover to see the glare sweep</CardDescription> </CardContent> </Card></GlareHover>
14-day free trial · No card required
Join 4,000+ teams already using our platform to ship faster.
import { Button } from "@/components/sarahui/button";import { GlareHover } from "@/components/sarahui/glare-hover"; export default function Cta() { return ( <GlareHover className="rounded-xl w-full" duration={700}> <div className="p-8 text-center border rounded-xl bg-surface"> <p className="text-muted text-xs uppercase font-semibold">14-day free trial · No card required</p> <h3 className="text-2xl font-bold mt-2">Ready to get started?</h3> <p className="text-muted text-sm mt-2">Join 4,000+ teams already using our platform to ship faster.</p> <div className="flex justify-center gap-2 mt-4"> <Button variant="primary">Start free trial</Button> <Button variant="outline">View demo</Button> </div> </div> </GlareHover> );}
Your plan expires in 3 days. Renew to keep access.
Check your card details and try again.
You have full Pro access until April 5, 2027.
import { Alert } from "@/components/sarahui/alert";import { GlareHover } from "@/components/sarahui/glare-hover"; export default function Alerts() { return ( <div className="flex flex-col gap-3"> <GlareHover className="w-full rounded-lg" duration={550} opacity={0.25}> <Alert title="Action required" description="Your plan expires in 3 days. Renew to keep access." tone="info" /> </GlareHover> <GlareHover className="w-full rounded-lg" color="#ff6070" opacity={0.25} duration={550}> <Alert title="Payment declined" description="Check your card details and try again." tone="danger" /> </GlareHover> <GlareHover className="w-full rounded-lg" color="#60ff70" opacity={0.25} duration={550}> <Alert title="Subscription confirmed" description="You have full Pro access until April 5, 2027." tone="success" /> </GlareHover> </div> );}
Configuration parameters and properties for GlareHover.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
any | - |
Card or UI element wrapped by the interactive glare layer |
color |
string | 'rgba(255, 255, 255, 0.4)' |
Color and opacity of the 3D specular glare highlight sweep |
opacity |
number | 0.4 |
Maximum opacity level of specular glare highlight |
background |
string | '' |
Optional custom background styling |
angle |
number | -45 |
Linear angle in degrees across which glare light sweeps |
size |
number | 100 |
Width of glare reflection band as percentage |
duration |
number | 650 |
Transition duration in milliseconds for glare sweep |
playOnce |
boolean | false |
Whether glare sweep plays only on initial hover |
width |
number | string | '' |
Container width override |
height |
number | string | '' |
Container height override |
className |
string | '' |
Optional CSS class for glare hover container |
class |
string | '' |
Alternative class name property |
attrs |
Record<string, any> | {} |
Additional HTML attributes for glare container |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.GlareHover({ children: UI.html`<div style="padding:20px;border:1px solid var(--s-border-default);">Interactive Glare Card</div>` }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:40px 16px;min-height:360px;"> <div style="width:340px;max-width:100%;"> <div class="s-glare-hover rounded-xl w-full" style="--gh-angle:-45deg;--gh-duration:600ms;--gh-size:250%;--gh-color:#ffffff;--gh-opacity:0.35;"><div class="s-glare-hover-surface"> <div style="width:100%;box-sizing:border-box;padding:24px;border-radius:16px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);box-shadow:0 4px 16px rgba(0,0,0,0.06);text-align:left;"> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:6px;"> <h3 style="font-size:18px;font-weight:700;margin:0;">Pro</h3> <span class="s-badge s-tone-brand"><i class="s-status-dot" aria-hidden="true"></i>Popular</span> </div> <p class="s-small s-muted" style="margin:0 0 16px;">For teams that need more.</p> <div style="display:flex;align-items:baseline;gap:4px;margin-bottom:20px;"> <span style="font-size:36px;font-weight:800;letter-spacing:-0.03em;color:var(--s-text-primary);">$49</span> <span class="s-small s-muted">/mo</span> </div> <div style="display:flex;flex-direction:column;gap:10px;margin-bottom:24px;font-size:13px;"> <div style="display:flex;align-items:center;gap:8px;"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4 12 5 5L20 6"/></svg> <span>Unlimited projects</span></div> <div style="display:flex;align-items:center;gap:8px;"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4 12 5 5L20 6"/></svg> <span>Team collaboration</span></div> <div style="display:flex;align-items:center;gap:8px;"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4 12 5 5L20 6"/></svg> <span>Advanced analytics</span></div> <div style="display:flex;align-items:center;gap:8px;color:var(--s-text-muted);"><span style="width:15px;text-align:center;">•</span> <span>SSO (coming soon)</span></div> </div> <button type="button" class="s-button s-button--primary s-button--md s-button--full"><span>Get started</span></button> </div> </div><div class="s-glare-hover-glare" aria-hidden="true"></div></div> </div> </div> Atmospheric meteor streaks showering through background surfaces.
import { Meteors } from "@/components/sarahui/meteors" export default function MeteorDemo() { return ( <div className="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border"> <Meteors number={30} /> <span className="pointer-events-none bg-linear-to-b from-black to-gray-300/80 bg-clip-text text-center text-8xl leading-none font-semibold whitespace-pre-wrap text-transparent dark:from-white dark:to-slate-900/10"> Meteors </span> </div> )}
Add Meteors to your project.
$ npx sarahui add meteors
import { Meteors } from 'sarahui';
<div className="relative h-[500px] w-full overflow-hidden"> <Meteors /></div>
import { Meteors } from "@/components/sarahui/meteors"; export function MeteorsDemo() { return <Meteors number={16} />;}
import { Meteors } from "@/components/sarahui/meteors"; export function MeteorsSubtle() { return <Meteors number={6} />;}
Configuration parameters and properties for Meteors.
| Prop | Type | Default | Description |
|---|---|---|---|
number |
number | 20 |
Number of meteors |
minDelay |
number | 0.2 |
Minimum delay in seconds before meteor animation starts |
maxDelay |
number | 1.2 |
Maximum delay in seconds before meteor animation starts |
minDuration |
number | 2 |
Minimum duration in seconds for meteor animation |
maxDuration |
number | 10 |
Maximum duration in seconds for meteor animation |
angle |
number | 215 |
Angle in degrees for meteor trajectory |
className |
string | - |
Optional additional CSS classes |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Meteors({ number: 16 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;display:flex;height:400px;width:100%;flex-direction:column;align-items:center;justify-content:center;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);"> <span style="pointer-events:none;white-space:pre-wrap;background:linear-gradient(to bottom, var(--s-text-primary), color-mix(in srgb, var(--s-text-muted) 30%, transparent));-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-align:center;font-size:clamp(4rem, 10vw, 7rem);font-weight:800;line-height:1;letter-spacing:-0.05em;user-select:none;"> Meteors </span> <div class="s-meteors" aria-hidden="true"><span class="s-meteor" style="top:0;left:0%;animation-delay:0.0s;animation-duration:4.0s;"></span><span class="s-meteor" style="top:0;left:8%;animation-delay:0.4s;animation-duration:4.8s;"></span><span class="s-meteor" style="top:0;left:16%;animation-delay:0.8s;animation-duration:5.6s;"></span><span class="s-meteor" style="top:0;left:25%;animation-delay:1.2s;animation-duration:6.4s;"></span><span class="s-meteor" style="top:0;left:33%;animation-delay:1.6s;animation-duration:4.0s;"></span><span class="s-meteor" style="top:0;left:41%;animation-delay:0.0s;animation-duration:4.8s;"></span><span class="s-meteor" style="top:0;left:50%;animation-delay:0.4s;animation-duration:5.6s;"></span><span class="s-meteor" style="top:0;left:58%;animation-delay:0.8s;animation-duration:6.4s;"></span><span class="s-meteor" style="top:0;left:66%;animation-delay:1.2s;animation-duration:4.0s;"></span><span class="s-meteor" style="top:0;left:75%;animation-delay:1.6s;animation-duration:4.8s;"></span><span class="s-meteor" style="top:0;left:83%;animation-delay:0.0s;animation-duration:5.6s;"></span><span class="s-meteor" style="top:0;left:91%;animation-delay:0.4s;animation-duration:6.4s;"></span><span class="s-meteor" style="top:0;left:100%;animation-delay:0.8s;animation-duration:4.0s;"></span><span class="s-meteor" style="top:0;left:108%;animation-delay:1.2s;animation-duration:4.8s;"></span><span class="s-meteor" style="top:0;left:116%;animation-delay:1.6s;animation-duration:5.6s;"></span><span class="s-meteor" style="top:0;left:125%;animation-delay:0.0s;animation-duration:6.4s;"></span><span class="s-meteor" style="top:0;left:133%;animation-delay:0.4s;animation-duration:4.0s;"></span><span class="s-meteor" style="top:0;left:141%;animation-delay:0.8s;animation-duration:4.8s;"></span><span class="s-meteor" style="top:0;left:150%;animation-delay:1.2s;animation-duration:5.6s;"></span><span class="s-meteor" style="top:0;left:158%;animation-delay:1.6s;animation-duration:6.4s;"></span><span class="s-meteor" style="top:0;left:166%;animation-delay:0.0s;animation-duration:4.0s;"></span><span class="s-meteor" style="top:0;left:175%;animation-delay:0.4s;animation-duration:4.8s;"></span><span class="s-meteor" style="top:0;left:183%;animation-delay:0.8s;animation-duration:5.6s;"></span><span class="s-meteor" style="top:0;left:191%;animation-delay:1.2s;animation-duration:6.4s;"></span></div> </div> Interactive celebratory confetti particle burst on user click.
"use client" import { useRef } from "react" import { Confetti, type ConfettiRef } from "@/components/sarahui/confetti" export default function ConfettiDemo() { const confettiRef = useRef<ConfettiRef>(null) return ( <div className="bg-background relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border"> <span className="pointer-events-none bg-linear-to-b from-black to-gray-300/80 bg-clip-text text-center text-8xl leading-none font-semibold whitespace-pre-wrap text-transparent dark:from-white dark:to-slate-900/10"> Confetti </span> <Confetti ref={confettiRef} className="absolute top-0 left-0 z-0 size-full" onMouseEnter={() => { confettiRef.current?.fire({}) }} /> </div> )}
Add Confetti to your project.
$ npx sarahui add confetti
import { Confetti } from 'sarahui';
import { useRef } from "react"import { Confetti } from 'sarahui' export function ConfettiDemo() { const confettiRef = useRef(null) return ( <div> <Confetti ref={confettiRef} manualstart /> <button onClick={() => confettiRef.current?.fire()}> Trigger Confetti </button> </div> )}
import { Confetti } from "@/components/sarahui/confetti"; export function ConfettiFireworks() { const launch = () => { Confetti.fireworks(); }; return <button onClick={launch}>Launch Fireworks</button>;}
import { Confetti } from "@/components/sarahui/confetti"; export function ConfettiSideCannons() { const trigger = () => { Confetti.sideCannons(); }; return <button onClick={trigger}>Trigger Side Cannons</button>;}
Configuration parameters and properties for Confetti.
| Prop | Type | Default | Description |
|---|---|---|---|
ref |
React.Ref<ConfettiRef> | null |
Ref exposing imperative `fire(options?: ConfettiOptions)` method |
options |
ConfettiOptions | {} |
Default options for automatic or imperative confetti triggers |
globalOptions |
ConfettiGlobalOptions | { resize: true, useWorker: true } |
Global options passed to `canvas-confetti.create() |
manualstart |
Boolean | false |
Prevents automatic confetti firing on component mount |
className |
String | undefined |
Custom CSS classes applied to the canvas element |
| Prop | Type | Default | Description |
|---|---|---|---|
Method |
Parameters | Return Type |
Description |
fire |
options?: ConfettiOptions | Promise<void> | void |
Imperatively triggers a confetti burst on the canvas instance |
| Prop | Type | Default | Description |
|---|---|---|---|
Field |
Type | Default |
Description |
particleCount |
Integer | 50 |
Number of confetti particles to launch |
angle |
Number | 90 |
Launch angle in degrees |
spread |
Number | 45 |
Spread angle in degrees |
startVelocity |
Number | 45 |
Initial particle velocity |
decay |
Number | 0.9 |
Velocity decay rate |
gravity |
Number | 1 |
Gravity applied to particles |
drift |
Number | 0 |
Horizontal drift factor |
flat |
Boolean | false |
Renders flat 2D shapes |
ticks |
Number | 200 |
Number of animation frames confetti lasts |
origin |
{ x: number, y: number } | { x: 0.5, y: 0.5 } |
Normalized viewport origin coordinates (`0.0` to `1.0`) |
colors |
Array of Strings | HEX array |
Custom HEX color palette |
shapes |
Array of Strings | ['square', 'circle', 'star'] |
Particle shapes or custom text shapes |
zIndex |
Integer | 9999 |
Z-index of the confetti burst |
scalar |
Number | 1 |
Scaling factor for particle dimensions |
| Prop | Type | Default | Description |
|---|---|---|---|
options |
ConfettiOptions & ConfettiGlobalOptions | {} |
Options for the confetti launched on button click |
children |
React.ReactNode | null |
Button label and child elements |
ref |
React.Ref<HTMLButtonElement> | null |
Forwarded DOM reference to the underlying button |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Confetti({ label: "Trigger Confetti", icon: "spark" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div data-s-confetti="basic" data-confetti-hover style="position:relative;display:flex;height:400px;width:100%;flex-direction:column;align-items:center;justify-content:center;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);cursor:pointer;"> <span style="pointer-events:none;white-space:pre-wrap;background:linear-gradient(to bottom, var(--s-text-primary), color-mix(in srgb, var(--s-text-muted) 30%, transparent));-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-align:center;font-size:clamp(4rem, 10vw, 7rem);font-weight:800;line-height:1;letter-spacing:-0.05em;user-select:none;"> Confetti </span> </div> Interactive background particle field rendered on canvas.
import { Particles } from "@/components/sarahui/particles"; export function ParticlesDemo() { const color = "var(--s-brand-indigo, #5B3DF5)"; return ( <div className="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-background md:shadow-xl"> <span className="pointer-events-none z-10 text-center text-8xl leading-none font-semibold whitespace-pre-wrap"> Particles </span> <Particles className="absolute inset-0 z-0" quantity={100} ease={80} color={color} refresh /> </div> );}
Add Particles to your project.
$ npx sarahui add particles
import { Particles } from 'sarahui';
<div className="relative h-[500px] w-full overflow-hidden"> <Particles /></div>
import { Particles } from "@/components/sarahui/particles"; export function ParticlesDemo() { return <Particles quantity={40} />;}
import { Particles } from "@/components/sarahui/particles"; export function ParticlesAccent() { return <Particles quantity={25} color="#5B3DF5" />;}
Configuration parameters and properties for Particles.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | - |
The class name for the component |
quantity |
number | 100 |
The number of particles |
staticity |
number | 50 |
The staticity of the particles |
ease |
number | 50 |
The ease of the particles |
size |
number | 0.4 |
The size of the particles |
refresh |
boolean | false |
Whether to refresh the particles |
color |
string | #ffffff |
The color of the particles |
vx |
number | 0 |
The x velocity of the particles |
vy |
number | 0 |
The y velocity of the particles |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Particles({ quantity: 40, color: "var(--s-primary)" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;display:flex;height:400px;width:100%;flex-direction:column;align-items:center;justify-content:center;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);"> <span style="pointer-events:none;white-space:pre-wrap;background:linear-gradient(to bottom, var(--s-text-primary), color-mix(in srgb, var(--s-text-muted) 30%, transparent));-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-align:center;font-size:clamp(4rem, 10vw, 7rem);font-weight:800;line-height:1;letter-spacing:-0.05em;user-select:none;"> Particles </span> <div class="s-particles" data-s-particles="60" data-color=""><canvas class="s-particles-canvas"></canvas></div> </div> Smooth circular mask view-transition theme switcher with sun and moon icons.
import { AnimatedThemeToggler } from "@/components/sarahui/animated-theme-toggler" export default function AnimatedThemeTogglerDemo() { return ( <div className="flex justify-center p-6"> <AnimatedThemeToggler /> </div> )}
Add AnimatedThemeToggler to your project.
$ npx sarahui add animated-theme-toggler
import { AnimatedThemeToggler } from 'sarahui';
<AnimatedThemeToggler />
import { AnimatedThemeToggler } from "@/components/sarahui/animated-theme-toggler"; export function AnimatedThemeTogglerSquare() { return <AnimatedThemeToggler variant="square" />;}
import { AnimatedThemeToggler } from "@/components/sarahui/animated-theme-toggler"; export function AnimatedThemeTogglerDiamond() { return <AnimatedThemeToggler variant="diamond" />;}
Configuration parameters and properties for AnimatedThemeToggler.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | — |
Additional classes for the button |
duration |
number | 400 |
Duration of the theme transition animation in milliseconds |
variant |
TransitionVariant | "circle" |
Shape used for the view-transition clip-path reveal |
fromCenter |
boolean | false |
If true, the clip expands from the viewport center instead of the button |
theme |
"light" | "dark" | — |
Controlled theme value. When set, the parent owns persistence (e.g. `next-themes`). |
onThemeChange |
(theme: "light" | "dark") => void | — |
Called on toggle. Pair with `theme` for controlled usage. |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.AnimatedThemeToggler(); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="demo-wrap" style="align-items:center;gap:16px;"><button type="button" class="s-animated-theme-toggler s-button s-icon-button s-button--outline s-button--md" data-s-theme-toggler="circle" data-duration="400" data-from-center="false" aria-label="Toggle theme"><span class="s-theme-icon-sun"><svg class="s-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8ZM12 2v2m0 16v2M2 12h2m16 0h2M5 5l1.5 1.5m11 11L19 19M5 19l1.5-1.5m11-11L19 5"/></svg></span><span class="s-theme-icon-moon"><svg class="s-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20.5 14A8.6 8.6 0 0 1 10 3.5 8.6 8.6 0 1 0 20.5 14Z"/></svg></span></button><span class="s-small s-muted">Click to toggle dark/light mode with circular ripple transition</span></div>Smooth blur-to-focus entrance transition with gentle translation.
import { BlurFade } from "@/components/sarahui/blur-fade" const images = Array.from({ length: 9 }, (_, i) => { const isLandscape = i % 2 === 0 const width = isLandscape ? 800 : 600 const height = isLandscape ? 600 : 800 return `https://picsum.photos/seed/${i + 1}/${width}/${height}`}) export default function BlurFadeDemo() { return ( <section id="photos"> <div className="columns-2 gap-4 sm:columns-3"> {images.map((imageUrl, idx) => ( <BlurFade key={imageUrl} delay={0.25 + idx * 0.05} inView> <img className="mb-4 size-full rounded-lg object-contain" src={imageUrl} alt={`Random stock image ${idx + 1}`} /> </BlurFade> ))} </div> </section> )}
Add BlurFade to your project.
$ npx sarahui add blur-fade
import { BlurFade } from 'sarahui';
<BlurFade> <img src="https://picsum.photos/300/200?random=1" alt="Sample 1" /> <img src="https://picsum.photos/300/200?random=2" alt="Sample 2" /> <img src="https://picsum.photos/300/200?random=3" alt="Sample 3" /></BlurFade>
import { BlurFade } from "@/components/sarahui/blur-fade"; export function BlurFadeDemo() { return ( <BlurFade delay={0.2}> <h3>Smooth Content Entrance</h3> </BlurFade> );}
import { BlurFade } from "@/components/sarahui/blur-fade"; export function BlurFadeHero() { return ( <BlurFade delay={0.1}> <h1>Welcome to sarahUI</h1> </BlurFade> );}
Configuration parameters and properties for BlurFade.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
React.ReactNode | - |
The content to be animated |
className |
string | - |
The class name to be applied to the component |
variant |
object | - |
Custom animation variants for motion component |
duration |
number | 0.4 |
Duration (seconds) for the animation |
delay |
number | 0 |
Delay (seconds) before the animation starts |
offset |
number | 6 |
Offset for the animation |
direction |
string | "down" |
Direction for the animation (`up`, `down`, `left`, `right`) |
inView |
boolean | false |
Whether to trigger animation when component is in view |
inViewMargin |
MarginType | "-50px" |
Margin for triggering the in-view animation |
blur |
string | "6px" |
Amount of blur to apply during the animation |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.BlurFade({ children: "Smooth blur fade-in entrance transition effect" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="width:100%;max-width:560px;margin:0 auto;padding:16px 0;"> <div style="columns:3;column-gap:12px;"> <div style="margin-bottom:12px;break-inside:avoid;"> <div class="s-blur-fade" style="animation-delay:0s;animation-duration:0.6s;"><img src="assets/photo-1.svg" alt="Stock photo 1" style="width:100%;display:block;border-radius:10px;object-fit:cover;box-shadow:0 1px 3px rgba(0,0,0,0.06);"></div> </div> <div style="margin-bottom:12px;break-inside:avoid;"> <div class="s-blur-fade" style="animation-delay:0.12s;animation-duration:0.6s;"><img src="assets/photo-2.svg" alt="Stock photo 2" style="width:100%;display:block;border-radius:10px;object-fit:cover;box-shadow:0 1px 3px rgba(0,0,0,0.06);"></div> </div> <div style="margin-bottom:12px;break-inside:avoid;"> <div class="s-blur-fade" style="animation-delay:0.24s;animation-duration:0.6s;"><img src="assets/photo-3.svg" alt="Stock photo 3" style="width:100%;display:block;border-radius:10px;object-fit:cover;box-shadow:0 1px 3px rgba(0,0,0,0.06);"></div> </div> <div style="margin-bottom:12px;break-inside:avoid;"> <div class="s-blur-fade" style="animation-delay:0.36s;animation-duration:0.6s;"><img src="assets/photo-4.svg" alt="Stock photo 4" style="width:100%;display:block;border-radius:10px;object-fit:cover;box-shadow:0 1px 3px rgba(0,0,0,0.06);"></div> </div> <div style="margin-bottom:12px;break-inside:avoid;"> <div class="s-blur-fade" style="animation-delay:0.48s;animation-duration:0.6s;"><img src="assets/photo-5.svg" alt="Stock photo 5" style="width:100%;display:block;border-radius:10px;object-fit:cover;box-shadow:0 1px 3px rgba(0,0,0,0.06);"></div> </div> <div style="margin-bottom:12px;break-inside:avoid;"> <div class="s-blur-fade" style="animation-delay:0.6s;animation-duration:0.6s;"><img src="assets/photo-6.svg" alt="Stock photo 6" style="width:100%;display:block;border-radius:10px;object-fit:cover;box-shadow:0 1px 3px rgba(0,0,0,0.06);"></div> </div> </div> </div> Staggered fluid character entrance with configurable fade, slide, and blur effects.
import { TextAnimate } from "@/components/sarahui/text-animate" export default function TextAnimateDemo() { return ( <TextAnimate animation="blurInUp" by="character" once> Blur in by character </TextAnimate> )}
Add TextAnimate to your project.
$ npx sarahui add text-animate
import { TextAnimate } from 'sarahui';
<TextAnimate animation="blurInUp" by="word"> Blur in by word</TextAnimate>
import { TextAnimate } from "@/components/sarahui/text-animate"; export function TextAnimateBlurIn() { return <TextAnimate animation="blurIn" by="text">Thoughtful Motion Design</TextAnimate>;}
import { TextAnimate } from "@/components/sarahui/text-animate"; export function TextAnimateSlideUp() { return <TextAnimate animation="slideUp" by="word">Next Generation Web Primitives</TextAnimate>;}
Configuration parameters and properties for TextAnimate.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
string | - |
The text content to animate |
className |
string | - |
The class name to be applied to the component |
segmentClassName |
string | - |
The class name to be applied to each segment |
delay |
number | 0 |
Delay before animation starts |
duration |
number | 0.3 |
Duration of the animation |
variants |
Variants | - |
Custom motion variants for the animation |
as |
"article" | "div" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "li" | "p" | "section" | "span" | "p" |
The element type to render |
by |
"text" | "word" | "character" | "line" | "word" |
How to split the text ("text", "word", "character") |
startOnView |
boolean | true |
Whether to start animation when component enters viewport |
once |
boolean | false |
Whether to animate only once |
animation |
AnimationVariant | "fadeIn" |
The animation preset to use |
accessible |
boolean | true |
Whether to render an accessible screen-reader label |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.TextAnimate({ text: "Fluid Staggered Text" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="font-size:22px;font-weight:700;text-align:center;padding:24px;"><div class="s-text-animate s-text-animate--word"><span class="s-anim-segment s-anim-blurInUp" style="animation-delay:0.00s;animation-duration:0.48s;" aria-hidden="true">Fluid</span><span class="s-anim-segment s-anim-blurInUp" style="animation-delay:0.09s;animation-duration:0.48s;" aria-hidden="true"> </span><span class="s-anim-segment s-anim-blurInUp" style="animation-delay:0.17s;animation-duration:0.48s;" aria-hidden="true">Staggered</span><span class="s-anim-segment s-anim-blurInUp" style="animation-delay:0.26s;animation-duration:0.48s;" aria-hidden="true"> </span><span class="s-anim-segment s-anim-blurInUp" style="animation-delay:0.34s;animation-duration:0.48s;" aria-hidden="true">Letter</span><span class="s-anim-segment s-anim-blurInUp" style="animation-delay:0.43s;animation-duration:0.48s;" aria-hidden="true"> </span><span class="s-anim-segment s-anim-blurInUp" style="animation-delay:0.51s;animation-duration:0.48s;" aria-hidden="true">Animations</span><span class="s-sr-only">Fluid Staggered Letter Animations</span></div></div>Realistic typewriter animation with blinking cursor for engaging slogans.
import { TypingAnimation } from "@/components/sarahui/typing-animation" export default function Component() { return <TypingAnimation>Hello World! 👋</TypingAnimation>}
Add TypingAnimation to your project.
$ npx sarahui add typing-animation
import { TypingAnimation } from 'sarahui';
<TypingAnimation>Hello World! 👋</TypingAnimation>
import { TypingAnimation } from "@/components/sarahui/typing-animation"; export function TypingAnimationLoop() { return ( <TypingAnimation words={["Design systems", "Accessible patterns", "Zero dependencies"]} loop /> );}
import { TypingAnimation } from "@/components/sarahui/typing-animation"; export function TypingAnimationFast() { return ( <TypingAnimation text="Speed. Precision. Zero bloat." duration={40} /> );}
Configuration parameters and properties for TypingAnimation.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
string | - |
Single string to animate |
words |
string[] | - |
Array of strings to type and delete in sequence |
className |
string | - |
The class name to be applied to the component |
duration |
number | 100 |
Duration for each character |
typeSpeed |
number | 100 |
Speed of typing animation (ms per character) |
deleteSpeed |
number | 50 |
Speed of deleting animation (ms per character) |
delay |
number | 0 |
Delay before animation starts (in ms) |
pauseDelay |
number | 1000 |
Pause duration between words (in ms) |
loop |
boolean | false |
Whether to loop the animation |
as |
"article" | "div" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "li" | "p" | "section" | "span" | "span" |
Component to render as |
startOnView |
boolean | true |
Start animation when component is in view |
showCursor |
boolean | true |
Whether to show the typing cursor |
blinkCursor |
boolean | true |
Whether the cursor should blink |
cursorStyle |
"line" | "block" | "underscore" | "line" |
Style of the cursor (like VSCode) |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.TypingAnimation({ text: "sarahUI is crafted with precision, motion, and speed." }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:260px;"> <h2 style="font-size:clamp(2rem, 5vw, 3rem);font-weight:700;letter-spacing:-0.03em;text-align:center;color:var(--s-text-primary);margin:0;"> <span class="s-typing-animation s-terminal-typing" data-s-typing="Hello World! 👋" data-text="Hello World! 👋" data-s-terminal-item="typing" data-duration="60"><span class="s-typing-text"></span><span class="s-typing-cursor" aria-hidden="true"></span></span> </h2> </div> Layered 3D isometric sharp cast shadow typography.
import { LineShadowText } from "@/components/sarahui/line-shadow-text"; export function LineShadowTextDemo() { const shadowColor = "var(--s-brand-indigo, #5B3DF5)"; return ( <h1 className="text-5xl leading-none font-semibold tracking-tighter text-balance sm:text-6xl md:text-7xl lg:text-8xl"> Ship <LineShadowText className="italic" shadowColor={shadowColor}> Fast </LineShadowText> </h1> );}
Add LineShadowText to your project.
$ npx sarahui add line-shadow-text
import { LineShadowText } from 'sarahui';
<LineShadowText>SarahUI</LineShadowText>
import { LineShadowText } from "@/components/sarahui/line-shadow-text"; export function LineShadowTextDemo() { return <LineShadowText shadowColor="#5B3DF5">Isometric</LineShadowText>;}
import { LineShadowText } from "@/components/sarahui/line-shadow-text"; export function LineShadowTextRose() { return <LineShadowText shadowColor="#FF2E93">Velocity</LineShadowText>;}
Configuration parameters and properties for LineShadowText.
| Prop | Type | Default | Description |
|---|---|---|---|
shadowColor |
string | "black" |
The color of the shadow effect |
children |
string | - |
The text to display with shadow effect |
as |
"article" | "div" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "li" | "p" | "section" | "span" | "span" |
The HTML element to render the text as |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.LineShadowText({ text: "3D SHADOW TEXT" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:260px;"> <h1 style="font-size:clamp(3rem, 7vw, 5.5rem);font-weight:800;letter-spacing:-0.04em;text-align:center;color:var(--s-text-primary);margin:0;"> Ship <span style="font-style:italic;"><span class="s-line-shadow-text" data-text="Fast" style="--shadow-color:#5B3DF5;">Fast</span></span> </h1> </div> Northern lights aurora borealis ambient color wave typography.
import { AuroraText } from "@/components/sarahui/aurora-text" export default function AuroraTextDemo() { return ( <h1 className="text-4xl font-bold tracking-tighter md:text-5xl lg:text-7xl"> Ship <AuroraText>beautiful</AuroraText> </h1> )}
Add AuroraText to your project.
$ npx sarahui add aurora-text
import { AuroraText } from 'sarahui';
<AuroraText>Aurora Text</AuroraText>
import { AuroraText } from "@/components/sarahui/aurora-text"; export function AuroraTextDemo() { return ( <h1 className="text-4xl font-bold"> Ship <AuroraText>Beautiful Apps</AuroraText> </h1> );}
import { AuroraText } from "@/components/sarahui/aurora-text"; export function AuroraTextSunset() { return <AuroraText colors={["#FF4E50", "#F9D423", "#FF6B6B"]}>Golden Horizon</AuroraText>;}
Configuration parameters and properties for AuroraText.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | "" |
The class name to be applied to the component |
children |
ReactNode | - |
Children elements |
colors |
string[] | ["#FF0080", "#7928CA", "#0070F3", "#38bdf8"] |
Array of colors used for the aurora effect |
speed |
number | 1 |
Animation speed multiplier (1 is default, 2 is twice as fast) |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.AuroraText({ text: "Aurora Borealis Gradient" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:260px;"> <h1 style="font-size:clamp(2.5rem, 6vw, 4.5rem);font-weight:800;letter-spacing:-0.04em;line-height:1.1;text-align:center;color:var(--s-text-primary);margin:0;"> Ship <span class="s-aurora-text">beautiful</span> </h1> </div> High-impact headline typography filled dynamically with an underlying video texture.
import { VideoText } from "@/components/sarahui/video-text" export default function VideoTextDemo() { return ( <div className="relative h-[200px] w-full overflow-hidden"> <VideoText> OCEAN </VideoText> </div> )}
Add VideoText to your project.
$ npx sarahui add video-text
import { VideoText } from 'sarahui';
<div className="relative h-[500px] w-full overflow-hidden"> <VideoText>OCEAN</VideoText></div>
import { VideoText } from "sarahui"; export function VideoTextDemo() { return <VideoText text="CREATIVE" videoSrc="/ambient.mp4" />;}
import { VideoText } from "sarahui"; export function VideoTextFuture() { return <VideoText text="FUTURE" videoSrc="/future.mp4" fontSize="4.5rem" />;}
Configuration parameters and properties for VideoText.
| Prop | Type | Default | Description |
|---|---|---|---|
src |
string | Required |
The video source URL |
as |
ElementType | "div" |
The element type to render for the text |
children |
ReactNode | Required |
The content to display (will have the video "inside" it) |
className |
string | "" |
Additional className for the container |
autoPlay |
boolean | true |
Whether to autoplay the video |
muted |
boolean | true |
Whether to mute the video |
loop |
boolean | true |
Whether to loop the video |
preload |
"auto" | "metadata" | "none" | "auto" |
Whether to preload the video |
fontSize |
string | number | "120" |
Font size for the text mask |
fontWeight |
string | number | "bold" |
Font weight for the text mask |
textAnchor |
string | "middle" |
Text anchor for the text mask |
dominantBaseline |
string | "middle" |
Dominant baseline for the text mask |
fontFamily |
string | "sans-serif" |
Font family for the text mask |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.VideoText({ text: "CREATIVE" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;justify-content:center;align-items:center;padding:48px 16px;min-height:300px;background:var(--s-bg-surface);border-radius:12px;border:1px solid var(--s-border-default);"> <div class="s-video-text"><span class="s-video-text-fallback" style="position:absolute;font-size:clamp(3.5rem,8vw,6rem);font-weight:900;letter-spacing:0.02em;background:linear-gradient(135deg,#0284c7,#0d9488,#a855f7);-webkit-background-clip:text;-webkit-text-fill-color:transparent;pointer-events:none;z-index:0;">OCEAN</span></div> </div> Fluid numeric counter animation that smoothly counts up to any target.
import { NumberTicker } from "@/components/sarahui/number-ticker" export default function NumberTickerDemo() { return ( <NumberTicker value={100} className="text-8xl font-medium tracking-tighter whitespace-pre-wrap text-black dark:text-white" /> )}
Add NumberTicker to your project.
$ npx sarahui add number-ticker
import { NumberTicker } from 'sarahui';
<NumberTicker value={100} />
import { NumberTicker } from "@/components/sarahui/number-ticker"; export function NumberTickerDemo() { return <NumberTicker value={100} />;}
import { NumberTicker } from "@/components/sarahui/number-ticker"; export function NumberTickerDemo2() { return <NumberTicker value={100} startValue={80} />;}
import { NumberTicker } from "@/components/sarahui/number-ticker"; export function NumberTickerDecimalDemo() { return <NumberTicker value={5.67} decimalPlaces={2} />;}
Configuration parameters and properties for NumberTicker.
| Prop | Type | Default | Description |
|---|---|---|---|
value |
int | 0 |
The value to count to |
direction |
up | down | "up" |
The direction to count in |
delay |
number | 0 |
The delay before counting |
decimalPlaces |
number | 0 |
The number of decimal places to show |
startValue |
number | 0 |
The value to start counting from |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.NumberTicker({ value: 319 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:240px;"><span style="font-size:clamp(5rem,15vw,8rem);font-weight:500;letter-spacing:-0.05em;color:var(--s-text-primary);line-height:1;"><span class="s-number-ticker" data-s-number-ticker="100" data-direction="up" data-delay="0" data-decimals="0" data-start-value="0">100</span></span></div>High-end shimmering metallic light beam sweeping across text.
import { ArrowRightIcon } from "sarahui/icons" import { cn } from "sarahui/utils"import { AnimatedShinyText } from "@/components/sarahui/animated-shiny-text" export default function AnimatedShinyTextDemo() { return ( <div className="z-10 flex min-h-64 items-center justify-center"> <div className={cn( "group rounded-full border border-black/5 bg-neutral-100 text-base text-white transition-all ease-in hover:cursor-pointer hover:bg-neutral-200 dark:border-white/5 dark:bg-neutral-900 dark:hover:bg-neutral-800" )} > <AnimatedShinyText className="inline-flex items-center justify-center px-4 py-1 transition ease-out hover:text-neutral-600 hover:duration-300 hover:dark:text-neutral-400"> <span>✨ Introducing sarahUI</span> <ArrowRightIcon className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" /> </AnimatedShinyText> </div> </div> )}
Add AnimatedShinyText to your project.
$ npx sarahui add animated-shiny-text
import { AnimatedShinyText } from 'sarahui';
<AnimatedShinyText>Hello</AnimatedShinyText>
import { AnimatedShinyText } from "@/components/sarahui/animated-shiny-text"; export function AnimatedShinyTextDemo() { return ( <div className="rounded-full border bg-neutral-100 px-4 py-1.5 dark:bg-neutral-900"> <AnimatedShinyText>✨ Introducing SarahUI Pro</AnimatedShinyText> </div> );}
import { AnimatedShinyText } from "@/components/sarahui/animated-shiny-text"; export function AnimatedShinyTextMilestone() { return <AnimatedShinyText>🚀 120 Interactive Components</AnimatedShinyText>;}
Configuration parameters and properties for AnimatedShinyText.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
node | - |
The text to be shimmered. |
className |
string | - |
The class name to be applied to the shimmer. |
shimmerWidth |
number | 100 |
The width of the shimmer in pixels. |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.AnimatedShinyText({ text: "Luminous metallic shimmer effect" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:220px;"> <div class="s-anim-shiny-pill" style="display:inline-flex;align-items:center;gap:6px;padding:6px 18px;border-radius:9999px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);box-shadow:0 1px 3px rgba(0,0,0,0.04);cursor:pointer;"> <span>✨</span> <span class="s-shiny-text" style="--shimmer-width:100px">Introducing sarahUI</span> <svg class="s-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"/></svg> </div> </div> Continuous multi-stop radiant chromatic wave across typography.
import { ChevronRight } from "sarahui/icons" import { cn } from "sarahui/utils"import { AnimatedGradientText } from "@/components/sarahui/animated-gradient-text" export default function AnimatedGradientTextDemo() { return ( <div className="group relative mx-auto flex items-center justify-center rounded-full px-4 py-1.5 shadow-[inset_0_-8px_10px_#8fdfff1f] transition-shadow duration-500 ease-out hover:shadow-[inset_0_-5px_10px_#8fdfff3f]"> <span className={cn( "animate-gradient absolute inset-0 block h-full w-full rounded-[inherit] bg-gradient-to-r from-[#ffaa40]/50 via-[#9c40ff]/50 to-[#ffaa40]/50 bg-[length:300%_100%] p-[1px]" )} style={{ WebkitMask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)", WebkitMaskComposite: "destination-out", mask: "linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0)", maskComposite: "subtract", WebkitClipPath: "padding-box", }} /> 🎉 <hr className="mx-2 h-4 w-px shrink-0 bg-neutral-500" /> <AnimatedGradientText className="text-sm font-medium"> Introducing sarahUI </AnimatedGradientText> <ChevronRight className="ml-1 size-4 stroke-neutral-500 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" /> </div> )}
Add AnimatedGradientText to your project.
$ npx sarahui add animated-gradient-text
import { AnimatedGradientText } from 'sarahui';
<AnimatedGradientText>Animated Gradient Text</AnimatedGradientText>
import { AnimatedGradientText } from "@/components/sarahui/animated-gradient-text"; export function AnimatedGradientTextDemo() { return ( <AnimatedGradientText colorFrom="#ffaa40" colorTo="#9c40ff"> 🎉 50k+ Developers </AnimatedGradientText> );}
import { AnimatedGradientText } from "@/components/sarahui/animated-gradient-text"; export function AnimatedGradientTextFast() { return <AnimatedGradientText colorFrom="#00E5FF" colorTo="#10B981" speed={2}>⚡ Realtime Sync Active</AnimatedGradientText>;}
Configuration parameters and properties for AnimatedGradientText.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
node | - |
The children passed into the component |
className |
string | - |
The class name to be applied |
speed |
number | 1 |
The speed of the gradient animation |
colorFrom |
string | "#ffaa40" |
The starting color of the gradient |
colorTo |
string | "#9c40ff" |
The ending color of the gradient |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.AnimatedGradientText({ text: "Radiant Multi-color Gradient Wave" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:220px;"> <div class="s-anim-gradient-pill" style="display:inline-flex;align-items:center;gap:8px;padding:6px 16px;border-radius:9999px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);box-shadow:0 1px 3px rgba(0,0,0,0.05);cursor:pointer;"> <span>🎉</span> <span style="height:14px;width:1px;background:var(--s-border-default);"></span> <span class="s-anim-gradient-text">Introducing sarahUI</span> <svg class="s-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"/></svg> </div> </div> Progressive scroll and view-triggered gradient mask text reveal.
import { TextReveal } from "@/components/sarahui/text-reveal" export default function TextRevealDemo() { return <TextReveal>sarahUI will change the way you design.</TextReveal>}
Add TextReveal to your project.
$ npx sarahui add text-reveal
import { TextReveal } from 'sarahui';
<TextReveal>SarahUI will change the way you design.</TextReveal>
import { TextReveal } from "@/components/sarahui/text-reveal"; export function TextRevealDemo() { return ( <TextReveal text="sarahUI brings pure CSS tokens and native browser primitives together in perfect harmony." /> );}
import { TextReveal } from "@/components/sarahui/text-reveal"; export function TextRevealHero() { return ( <TextReveal text="Design without compromise. 120 components crafted for modern web experiences." /> );}
Configuration parameters and properties for TextReveal.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | '' |
Optional CSS class for styling overrides |
children |
ReactNode | - |
Children elements or content |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.TextReveal({ text: "Beautiful design system built with pure Web APIs." }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 24px;min-height:280px;text-align:center;"> <h2 style="font-size:clamp(2rem, 5vw, 3.5rem);font-weight:800;letter-spacing:-0.03em;line-height:1.2;color:var(--s-text-primary);margin:0;max-width:700px;"> <div class="s-text-reveal"><span class="s-reveal-word" style="--word-index:0"><span class="s-reveal-word-ghost">sarahUI </span><span class="s-reveal-word-fill">sarahUI </span></span><span class="s-reveal-word" style="--word-index:1"><span class="s-reveal-word-ghost">will </span><span class="s-reveal-word-fill">will </span></span><span class="s-reveal-word" style="--word-index:2"><span class="s-reveal-word-ghost">change </span><span class="s-reveal-word-fill">change </span></span><span class="s-reveal-word" style="--word-index:3"><span class="s-reveal-word-ghost">the </span><span class="s-reveal-word-fill">the </span></span><span class="s-reveal-word" style="--word-index:4"><span class="s-reveal-word-ghost">way </span><span class="s-reveal-word-fill">way </span></span><span class="s-reveal-word" style="--word-index:5"><span class="s-reveal-word-ghost">you </span><span class="s-reveal-word-fill">you </span></span><span class="s-reveal-word" style="--word-index:6"><span class="s-reveal-word-ghost">design. </span><span class="s-reveal-word-fill">design. </span></span></div> </h2> </div> High-impact diagonal gradient wipe transition revealing headline typography.
import { DiaTextReveal } from "@/components/sarahui/dia-text-reveal" export default function DiaTextRevealDemo() { return ( <div className="flex min-h-64 items-center justify-center p-8"> <DiaTextReveal className="text-4xl font-bold tracking-tight" text="sarahUI" colors={["#A97CF8", "#F38CB8", "#FDCC92"]} /> </div> )}
Add DiaTextReveal to your project.
$ npx sarahui add dia-text-reveal
import { DiaTextReveal } from 'sarahui';
<DiaTextReveal text="SarahUI" />
import { DiaTextReveal } from "@/components/sarahui/dia-text-reveal"; export function DiaTextRevealDemo() { return <DiaTextReveal text="Uncompromising Quality" />;}
import { DiaTextReveal } from "@/components/sarahui/dia-text-reveal"; export function DiaTextRevealGradient() { return <DiaTextReveal text="Next Level Motion" gradient="linear-gradient(135deg, #00E5FF, #FF0055)" />;}
Configuration parameters and properties for DiaTextReveal.
| Prop | Type | Default | Description |
|---|---|---|---|
text |
string | string[] | — |
Text to display. Use an array to rotate between strings when `repeat` is on. |
colors |
string[] | ["#c679c4", "#fa3d1d", "#ffb005", "#e1e1fe", "#0358f7"] |
Colors in the sweeping gradient band. |
textColor |
string | var(--foreground) |
Solid text color after the animation (matches your theme by default). |
duration |
number | 1.5 |
Sweep duration in seconds. |
delay |
number | 0 |
Delay before the sweep starts, in seconds. |
repeat |
boolean | false |
When `text` is an array, advance to the next string after each cycle. |
repeatDelay |
number | 0.5 |
Pause in seconds before replaying or advancing (when `repeat` is true). |
startOnView |
boolean | true |
Start the animation when the element enters the viewport. |
once |
boolean | true |
Play only the first time (when `startOnView` is used). |
className |
string | — |
Additional CSS classes on the root `span`. |
fixedWidth |
boolean | false |
Lock width to the widest string when `text` is an array to reduce layout shift. |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.DiaTextReveal({ text: "DIAGONAL SWEEP" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="padding:24px;text-align:center;"><span class="s-dia-text-reveal" data-s-dia-reveal="[&quot;DIAGONAL MASK SWEEP REVEAL&quot;]" data-colors="#c679c4,#fa3d1d,#ffb005,#e1e1fe,#0358f7" data-duration="1.5" data-repeat="false">DIAGONAL MASK SWEEP REVEAL</span></div>Random character scrambling decoding animation on hover.
import { HyperText } from "@/components/sarahui/hyper-text" export default function HyperTextDemo() { return <HyperText>Hover Me!</HyperText>}
Add HyperText to your project.
$ npx sarahui add hyper-text
import { HyperText } from 'sarahui';
<HyperText>Hover me</HyperText>
import { HyperText } from "@/components/sarahui/hyper-text"; export function HyperTextDemo() { return <HyperText text="HACK THE MATRIX" duration={800} />;}
import { HyperText } from "@/components/sarahui/hyper-text"; export function HyperTextSystem() { return <HyperText text="SARAH_UI_SYSTEM_READY" duration={1200} />;}
Configuration parameters and properties for HyperText.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
string | - |
Text content to animate |
className |
string | - |
The class name to be applied to the component |
duration |
number | 800 |
Duration of the animation in milliseconds |
delay |
number | 0 |
Delay before animation starts (in ms) |
as |
"article" | "div" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "li" | "p" | "section" | "span" | "div" |
Component to render as |
startOnView |
boolean | false |
Start animation when component is in view |
animateOnHover |
boolean | true |
Enable hover animation |
characterSet |
string[] | A-Z |
Custom character set for scramble effect |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.HyperText({ text: "SARAHUI DESIGN" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:260px;"> <h1 style="font-size:clamp(2.5rem, 6vw, 4rem);font-weight:800;letter-spacing:0.04em;text-align:center;color:var(--s-text-primary);margin:0;"> <span class="s-hyper-text" data-s-hyper-text="HOVER ME !" data-duration="800">HOVER ME !</span> </h1> </div> Smooth vertical text switcher cycling through key terms.
import { WordRotate } from "@/components/sarahui/word-rotate" export default function WordRotateDemo() { return ( <WordRotate className="text-4xl font-bold text-black dark:text-white" words={["Word", "Rotate"]} /> )}
Add WordRotate to your project.
$ npx sarahui add word-rotate
import { WordRotate } from 'sarahui';
<WordRotate words={["Word", "Rotate"]} />
import { WordRotate } from "@/components/sarahui/word-rotate"; export function WordRotateDemo() { return <WordRotate words={["Design", "Build", "Ship", "Delight"]} />;}
import { WordRotate } from "@/components/sarahui/word-rotate"; export function WordRotateSnappy() { return ( <WordRotate words={["Accessible", "Responsive", "Performant", "Lightweight"]} duration={1500} /> );}
Configuration parameters and properties for WordRotate.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | - |
The class name to be applied to the component |
duration |
number | 2500 |
Duration of the animation |
words |
string[] | "" |
An array of words to rotate through |
motionProps |
HTMLMotionProps | {} |
An object containing motion animation props |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.WordRotate({ words: ["Adaptive", "Accessible", "Lightweight", "High-contrast"] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:260px;"> <h2 style="font-size:clamp(2.5rem, 6vw, 4rem);font-weight:800;letter-spacing:-0.04em;text-align:center;color:var(--s-text-primary);margin:0;"> <span class="s-word-rotate" data-s-word-rotate="["Word","Rotate"]" data-duration="2500"><span class="s-word-rotate-item">Word</span></span> </h2> </div> Scroll-accelerated marquee ticker whose speed scales with scroll velocity.
import { ScrollVelocityContainer, ScrollVelocityRow,} from "@/components/sarahui/scroll-based-velocity" export default function ScrollBasedVelocityDemo() { return ( <div className="relative flex w-full flex-col items-center justify-center overflow-hidden"> <ScrollVelocityContainer className="text-4xl font-bold tracking-[-0.02em] md:text-7xl md:leading-20"> <ScrollVelocityRow baseVelocity={20} direction={1}> Velocity Scroll </ScrollVelocityRow> <ScrollVelocityRow baseVelocity={20} direction={-1}> Velocity Scroll </ScrollVelocityRow> </ScrollVelocityContainer> <div className="from-background pointer-events-none absolute inset-y-0 left-0 w-1/4 bg-gradient-to-r"></div> <div className="from-background pointer-events-none absolute inset-y-0 right-0 w-1/4 bg-gradient-to-l"></div> </div> )}
Add ScrollBasedVelocity to your project.
$ npx sarahui add scroll-based-velocity
import { ScrollBasedVelocity } from 'sarahui';
<ScrollVelocityContainer className="text-4xl font-bold md:text-7xl"> <ScrollVelocityRow baseVelocity={20} direction={1}> Velocity Scroll </ScrollVelocityRow> <ScrollVelocityRow baseVelocity={20} direction={-1}> Velocity Scroll </ScrollVelocityRow></ScrollVelocityContainer>
import { ScrollBasedVelocity } from "@/components/sarahui/scroll-based-velocity"; export function ScrollBasedVelocityDemo() { return <ScrollBasedVelocity text="SARAHUI • ACCESSIBLE • LIGHTWEIGHT • ZERO RUNTIME DEPS •" defaultVelocity={3} />;}
import { ScrollBasedVelocity } from "@/components/sarahui/scroll-based-velocity"; export function ScrollBasedVelocityReverse() { return <ScrollBasedVelocity text="• REVERSE VELOCITY • ACCELERATED SCROLL • SMOOTH PHYSICS •" defaultVelocity={-4} />;}
Configuration parameters and properties for ScrollBasedVelocity.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | - |
The class name to be applied to the component |
children |
ReactNode | - |
Content to be animated |
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | - |
The class name to be applied to the row container |
children |
ReactNode | - |
Content to be duplicated and scrolled |
baseVelocity |
number | 5 |
Base scroll velocity percentage of content width |
direction |
1 | -1 | 1 |
Scroll direction (1 = left-to-right, -1 = reverse) |
scrollReactivity |
boolean | true |
Toggles scroll interactivity |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.ScrollBasedVelocity({ text: "SARAHUI VELOCITY MARQUEE" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="width:100%;overflow:hidden;padding:16px 0;"><div class="s-scroll-velocity" data-s-scroll-velocity data-base-velocity="5" data-direction="1"><div class="s-velocity-track s-velocity-track-1"><div class="s-velocity-slide"><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span></div><div class="s-velocity-slide" aria-hidden="true"><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span></div></div><div class="s-velocity-track s-velocity-track-2"><div class="s-velocity-slide"><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span></div><div class="s-velocity-slide" aria-hidden="true"><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span><span class="s-velocity-item">SARAHUI ACCESSIBLE DESIGN SYSTEM · </span></div></div></div></div>Heading typography decorated with glittering dynamic star sparkles.
import { SparklesText } from "@/components/sarahui/sparkles-text" export default function SparklesTextDemo() { return <SparklesText>sarahUI</SparklesText>}
Add SparklesText to your project.
$ npx sarahui add sparkles-text
import { SparklesText } from 'sarahui';
<SparklesText>Sparkles Text</SparklesText>
import { SparklesText } from "@/components/sarahui/sparkles-text"; export function SparklesTextDemo() { return <SparklesText text="Magical Components" />;}
import { SparklesText } from "@/components/sarahui/sparkles-text"; export function SparklesTextGold() { return <SparklesText text="Premium Design System" colors={{ first: "#FFD700", second: "#FFA500" }} />;}
Configuration parameters and properties for SparklesText.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
string | - |
The content to display |
className |
string | - |
The class name to be applied to the sparkles text. |
sparklesCount |
number | 10 |
sparkles count that appears on the text |
colors |
object | {first: '#A07CFE', second: '#FE8FB5'} |
the sparkles colors |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.SparklesText({ text: "Magical UI Library" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:260px;"> <h1 style="font-size:clamp(3rem, 7vw, 5rem);font-weight:800;letter-spacing:-0.04em;text-align:center;color:var(--s-text-primary);margin:0;"> <span class="s-sparkles-text"><span class="s-sparkle-star s-sparkle-0" style="top:15%;left:10%;animation-delay:0.00s;"></span><span class="s-sparkle-star s-sparkle-1" style="top:29%;left:32%;animation-delay:0.25s;"></span><span class="s-sparkle-star s-sparkle-2" style="top:43%;left:54%;animation-delay:0.50s;"></span><span class="s-sparkle-star s-sparkle-0" style="top:57%;left:76%;animation-delay:0.75s;"></span><span class="s-sparkle-star s-sparkle-1" style="top:71%;left:8%;animation-delay:1.00s;"></span><span class="s-sparkle-star s-sparkle-2" style="top:5%;left:30%;animation-delay:1.25s;"></span><span class="s-sparkles-content">sarahUI</span></span> </h1> </div> Smooth SVG goo filter liquid text morphing transitions.
import { MorphingText } from "@/components/sarahui/morphing-text" const texts = [ "Hello", "Morphing", "Text", "Animation", "React", "Component", "Smooth", "Transition", "Engaging",] export default function MorphingTextDemo() { return <MorphingText texts={texts} />}
Add MorphingText to your project.
$ npx sarahui add morphing-text
import { MorphingText } from 'sarahui';
<MorphingText texts={["Hello", "World"]} />
import { MorphingText } from "@/components/sarahui/morphing-text"; export function MorphingTextDemo() { return <MorphingText texts={["DESIGN", "ENGINEER", "DELIGHT"]} />;}
import { MorphingText } from "@/components/sarahui/morphing-text"; export function MorphingTextFast() { return <MorphingText texts={["SPEED", "SAFETY", "SCALE"]} morphTime={1} cooldownTime={0.5} />;}
Configuration parameters and properties for MorphingText.
| Prop | Type | Default | Description |
|---|---|---|---|
texts |
string[] | [] |
Array of texts to morph between |
className |
string? | "" |
Additional classes for the container |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.MorphingText({ texts: ["DESIGN", "ENGINEER", "EXPERIENCE"] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="text-align:center;padding:20px;display:flex;justify-content:center;"><div class="s-morphing-text-wrap"><svg class="s-morphing-svg" aria-hidden="true" style="position:fixed;height:0;width:0;"><defs><filter id="s-morph-10"><feColorMatrix in="SourceGraphic" type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 255 -140"/></filter></defs></svg><div class="s-morphing-text" style="filter:url(#s-morph-10) blur(0.6px)" data-s-morphing-text="["INNOVATION","ELEGANCE","PERFORMANCE","DELIGHT"]"><span class="s-morph-text-1">INNOVATION</span><span class="s-morph-text-2">ELEGANCE</span></div></div></div>Circular rotating typography spinning continuously along an SVG orbital path.
import { SpinningText } from "@/components/sarahui/spinning-text" export default function SpinningTextBasic() { return <SpinningText>learn more • earn more • grow more •</SpinningText>}
Add SpinningText to your project.
$ npx sarahui add spinning-text
import { SpinningText } from 'sarahui';
<SpinningText>learn more • earn more • grow more •</SpinningText>
import { SpinningText } from "@/components/sarahui/spinning-text"; export function SpinningTextDemo() { return <SpinningText radius={64} duration={10}>• SARAHUI • MODERN DESIGN • ZERO DEPS </SpinningText>;}
import { SpinningText } from "@/components/sarahui/spinning-text"; export function SpinningTextReverse() { return <SpinningText radius={80} duration={8} reverse>• REVERSE ROTATION • 60FPS MOTION </SpinningText>;}
Configuration parameters and properties for SpinningText.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
ReactElement | - |
The text content to be animated in a circular motion. |
style |
CSSProperties | {} |
Custom styles for the text container. |
duration |
number | 10 |
The duration of the full circular rotation animation. |
className |
string | - |
A custom class name for the text container. |
reverse |
boolean | false |
Determines if the animation should rotate in reverse. |
radius |
number | 5 |
The radius of the circular path for the text animation. |
transition |
Transition | - |
Custom transition effects for the animation. |
variants |
{ container?: Variants; item?: Variants; } | - |
Variants for container and item animations. |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.SpinningText({ text: "SARAHUI • MASARA X • DESIGN • " }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;justify-content:center;padding:24px;"><div class="s-spinning-text" style="--spin-dur:10s;"><span class="s-spinning-char" aria-hidden="true" style="--index:0;--total:37;--radius:5;">S</span><span class="s-spinning-char" aria-hidden="true" style="--index:1;--total:37;--radius:5;">A</span><span class="s-spinning-char" aria-hidden="true" style="--index:2;--total:37;--radius:5;">R</span><span class="s-spinning-char" aria-hidden="true" style="--index:3;--total:37;--radius:5;">A</span><span class="s-spinning-char" aria-hidden="true" style="--index:4;--total:37;--radius:5;">H</span><span class="s-spinning-char" aria-hidden="true" style="--index:5;--total:37;--radius:5;">U</span><span class="s-spinning-char" aria-hidden="true" style="--index:6;--total:37;--radius:5;">I</span><span class="s-spinning-char" aria-hidden="true" style="--index:7;--total:37;--radius:5;"> </span><span class="s-spinning-char" aria-hidden="true" style="--index:8;--total:37;--radius:5;">•</span><span class="s-spinning-char" aria-hidden="true" style="--index:9;--total:37;--radius:5;"> </span><span class="s-spinning-char" aria-hidden="true" style="--index:10;--total:37;--radius:5;">M</span><span class="s-spinning-char" aria-hidden="true" style="--index:11;--total:37;--radius:5;">A</span><span class="s-spinning-char" aria-hidden="true" style="--index:12;--total:37;--radius:5;">S</span><span class="s-spinning-char" aria-hidden="true" style="--index:13;--total:37;--radius:5;">A</span><span class="s-spinning-char" aria-hidden="true" style="--index:14;--total:37;--radius:5;">R</span><span class="s-spinning-char" aria-hidden="true" style="--index:15;--total:37;--radius:5;">A</span><span class="s-spinning-char" aria-hidden="true" style="--index:16;--total:37;--radius:5;"> </span><span class="s-spinning-char" aria-hidden="true" style="--index:17;--total:37;--radius:5;">X</span><span class="s-spinning-char" aria-hidden="true" style="--index:18;--total:37;--radius:5;"> </span><span class="s-spinning-char" aria-hidden="true" style="--index:19;--total:37;--radius:5;">•</span><span class="s-spinning-char" aria-hidden="true" style="--index:20;--total:37;--radius:5;"> </span><span class="s-spinning-char" aria-hidden="true" style="--index:21;--total:37;--radius:5;">D</span><span class="s-spinning-char" aria-hidden="true" style="--index:22;--total:37;--radius:5;">E</span><span class="s-spinning-char" aria-hidden="true" style="--index:23;--total:37;--radius:5;">S</span><span class="s-spinning-char" aria-hidden="true" style="--index:24;--total:37;--radius:5;">I</span><span class="s-spinning-char" aria-hidden="true" style="--index:25;--total:37;--radius:5;">G</span><span class="s-spinning-char" aria-hidden="true" style="--index:26;--total:37;--radius:5;">N</span><span class="s-spinning-char" aria-hidden="true" style="--index:27;--total:37;--radius:5;"> </span><span class="s-spinning-char" aria-hidden="true" style="--index:28;--total:37;--radius:5;">S</span><span class="s-spinning-char" aria-hidden="true" style="--index:29;--total:37;--radius:5;">Y</span><span class="s-spinning-char" aria-hidden="true" style="--index:30;--total:37;--radius:5;">S</span><span class="s-spinning-char" aria-hidden="true" style="--index:31;--total:37;--radius:5;">T</span><span class="s-spinning-char" aria-hidden="true" style="--index:32;--total:37;--radius:5;">E</span><span class="s-spinning-char" aria-hidden="true" style="--index:33;--total:37;--radius:5;">M</span><span class="s-spinning-char" aria-hidden="true" style="--index:34;--total:37;--radius:5;"> </span><span class="s-spinning-char" aria-hidden="true" style="--index:35;--total:37;--radius:5;">•</span><span class="s-spinning-char" aria-hidden="true" style="--index:36;--total:37;--radius:5;"> </span><span class="s-sr-only">SARAHUI • MASARA X • DESIGN SYSTEM • </span></div></div>Animated hand-drawn highlighter marker stroke emphasizing key phrases.
import { Highlighter } from "@/components/sarahui/highlighter" export default function HighlighterDemo() { return ( <div className="text-center"> <p className="leading-relaxed"> The{" "} <Highlighter action="underline" color="#FF9800"> sarahUI Highlighter </Highlighter>{" "} makes important{" "} <Highlighter action="highlight" color="#87CEFA"> text stand out </Highlighter>{" "} effortlessly. </p> </div> )}
Add Highlighter to your project.
$ npx sarahui add highlighter
import { Highlighter } from 'sarahui';
<p> The{" "} <Highlighter action="underline" color="#FF9800"> SarahUI Highlighter </Highlighter>{" "} makes important{" "} <Highlighter action="highlight" color="#87CEFA"> text stand out </Highlighter>{" "} effortlessly.</p>
import { Highlighter } from "@/components/sarahui/highlighter"; export function HighlighterDemo() { return <Highlighter color="rgba(91,61,245,0.35)">Thoughtful Design Systems</Highlighter>;}
import { Highlighter } from "@/components/sarahui/highlighter"; export function HighlighterUnderline() { return <Highlighter type="underline" color="rgba(255,46,147,0.4)">Underline Stroke Accent</Highlighter>;}
Configuration parameters and properties for Highlighter.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
React.ReactNode | Required |
The content to be highlighted/annotated. |
color |
string | "#ffd1dc" |
The color of the highlight. |
action |
"highlight" | "circle" | "box" | "bracket" | "crossed-off" | "strike-through" | "underline" | "highlight" |
The type of annotation effect to apply. |
strokeWidth |
number | 1.5px |
The width of the annotation stroke. |
animationDuration |
number | 500ms |
Duration of the animation in milliseconds. |
iterations |
number | 2 |
Number of times to draw the annotation (adds a sketchy effect when > 1). |
padding |
number | 2px |
Padding between the element and the annotation. |
multiline |
boolean | true |
Whether to annotate across multiple lines or treat content as a single line. |
isView |
boolean | false |
Controls whether the animation starts only when the element enters viewport. |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Highlighter({ children: "highlighted text" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="font-size:clamp(1.25rem, 3vw, 1.75rem);font-weight:600;line-height:1.8;padding:48px 16px;max-width:640px;margin:0 auto;text-align:center;color:var(--s-text-primary);"> The <span class="s-highlighter s-highlighter--underline" style="--highlight-color:#FF9800;--highlight-stroke:2px;" data-s-highlighter="underline"><span class="s-highlighter-content">sarahUI Highlighter</span><svg class="s-highlighter-svg" aria-hidden="true"><path class="s-highlighter-path" fill="none"></path></svg></span> makes important <span class="s-highlighter s-highlighter--highlight" style="--highlight-color:#87CEFA;--highlight-stroke:2px;" data-s-highlighter="highlight"><span class="s-highlighter-content">text stand out</span><svg class="s-highlighter-svg" aria-hidden="true"><path class="s-highlighter-path" fill="none"></path></svg></span> effortlessly. </div> 3D perspective card-flip animation alternating between front and back labels.
import Text3DFlip from "@/components/sarahui/text-3d-flip" export default function Text3DFlipDemo() { return ( <Text3DFlip className="bg-background font-serif text-2xl sm:text-5xl md:text-[56px]" textClassName="bg-background text-foreground" flipTextClassName="bg-background text-foreground" rotateDirection="top" staggerDuration={0.03} staggerFrom="first" transition={{ type: "spring", damping: 25, stiffness: 160 }} > Stay hungry, stay foolish </Text3DFlip> )}
Add Text3DFlip to your project.
$ npx sarahui add text3-dflip
import { Text3DFlip } from 'sarahui';
<Text3DFlip className="bg-background" textClassName="bg-background text-foreground" flipTextClassName="bg-background text-foreground" rotateDirection="top"> Stay hungry, stay foolish</Text3DFlip>
import { Text3DFlip } from "@/components/sarahui/text-3d-flip"; export function Text3DFlipDemo() { return <Text3DFlip words={["Modern", "Accessible", "Lightweight", "Powerful"]} />;}
import { Text3DFlip } from "@/components/sarahui/text-3d-flip"; export function Text3DFlipCenter() { return <Text3DFlip words={["Faster", "Smoother", "Cleaner", "Better"]} delay={1600} />;}
Configuration parameters and properties for Text3DFlip.
| Prop | Type | Default | Description |
|---|---|---|---|
children`\* |
ReactNode | - |
The content to be displayed and animated |
as |
ElementType | "p" |
HTML element to render the component as |
className |
string | - |
Class name for the main container element |
textClassName |
string | - |
Class name for each letter (visible before flip) |
flipTextClassName |
string | - |
Class name for each letter (revealed after flip) |
staggerDuration |
number | 0.05 |
Duration of stagger delay between each letter in seconds |
staggerFrom |
"first" | "last" | "center" | number | "random" | "first" |
Direction to stagger animations from. Use a number to stagger from a specific character index |
transition |
ValueAnimationTransition | AnimationOptions | { type: "spring", damping: 30, stiffness: 300 } |
Animation transition configuration from Motion |
rotateDirection |
"top" | "right" | "bottom" | "left" | "right" |
Direction of 3D rotation on hover |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Text3DFlip({ frontText: "HOVER TO FLIP", backText: "3D FLIP EFFECT" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;justify-content:center;align-items:center;padding:48px 16px;min-height:240px;"> <h2 style="font-size:clamp(2rem, 5vw, 3.5rem);font-weight:800;letter-spacing:-0.03em;text-align:center;color:var(--s-text-primary);margin:0;"> <div class="s-text-3d-flip s-flip--right" data-s-text-flip><span class="s-flip-word"><span class="s-flip-char-box" style="--char-i:0;"><span class="s-flip-front">S</span><span class="s-flip-back">S</span></span><span class="s-flip-char-box" style="--char-i:1;"><span class="s-flip-front">t</span><span class="s-flip-back">t</span></span><span class="s-flip-char-box" style="--char-i:2;"><span class="s-flip-front">a</span><span class="s-flip-back">a</span></span><span class="s-flip-char-box" style="--char-i:3;"><span class="s-flip-front">y</span><span class="s-flip-back">y</span></span></span><span class="s-flip-space"> </span><span class="s-flip-word"><span class="s-flip-char-box" style="--char-i:4;"><span class="s-flip-front">h</span><span class="s-flip-back">h</span></span><span class="s-flip-char-box" style="--char-i:5;"><span class="s-flip-front">u</span><span class="s-flip-back">u</span></span><span class="s-flip-char-box" style="--char-i:6;"><span class="s-flip-front">n</span><span class="s-flip-back">n</span></span><span class="s-flip-char-box" style="--char-i:7;"><span class="s-flip-front">g</span><span class="s-flip-back">g</span></span><span class="s-flip-char-box" style="--char-i:8;"><span class="s-flip-front">r</span><span class="s-flip-back">r</span></span><span class="s-flip-char-box" style="--char-i:9;"><span class="s-flip-front">y</span><span class="s-flip-back">y</span></span><span class="s-flip-char-box" style="--char-i:10;"><span class="s-flip-front">,</span><span class="s-flip-back">,</span></span></span><span class="s-flip-space"> </span><span class="s-flip-word"><span class="s-flip-char-box" style="--char-i:11;"><span class="s-flip-front">s</span><span class="s-flip-back">s</span></span><span class="s-flip-char-box" style="--char-i:12;"><span class="s-flip-front">t</span><span class="s-flip-back">t</span></span><span class="s-flip-char-box" style="--char-i:13;"><span class="s-flip-front">a</span><span class="s-flip-back">a</span></span><span class="s-flip-char-box" style="--char-i:14;"><span class="s-flip-front">y</span><span class="s-flip-back">y</span></span></span><span class="s-flip-space"> </span><span class="s-flip-word"><span class="s-flip-char-box" style="--char-i:15;"><span class="s-flip-front">f</span><span class="s-flip-back">f</span></span><span class="s-flip-char-box" style="--char-i:16;"><span class="s-flip-front">o</span><span class="s-flip-back">o</span></span><span class="s-flip-char-box" style="--char-i:17;"><span class="s-flip-front">o</span><span class="s-flip-back">o</span></span><span class="s-flip-char-box" style="--char-i:18;"><span class="s-flip-front">l</span><span class="s-flip-back">l</span></span><span class="s-flip-char-box" style="--char-i:19;"><span class="s-flip-front">i</span><span class="s-flip-back">i</span></span><span class="s-flip-char-box" style="--char-i:20;"><span class="s-flip-front">s</span><span class="s-flip-back">s</span></span><span class="s-flip-char-box" style="--char-i:21;"><span class="s-flip-front">h</span><span class="s-flip-back">h</span></span></span><span class="s-flip-space"> </span><span class="s-flip-word"><span class="s-flip-char-box" style="--char-i:22;"><span class="s-flip-front">S</span><span class="s-flip-back">S</span></span><span class="s-flip-char-box" style="--char-i:23;"><span class="s-flip-front">t</span><span class="s-flip-back">t</span></span><span class="s-flip-char-box" style="--char-i:24;"><span class="s-flip-front">a</span><span class="s-flip-back">a</span></span><span class="s-flip-char-box" style="--char-i:25;"><span class="s-flip-front">y</span><span class="s-flip-back">y</span></span></span><span class="s-flip-space"> </span><span class="s-flip-word"><span class="s-flip-char-box" style="--char-i:26;"><span class="s-flip-front">f</span><span class="s-flip-back">f</span></span><span class="s-flip-char-box" style="--char-i:27;"><span class="s-flip-front">o</span><span class="s-flip-back">o</span></span><span class="s-flip-char-box" style="--char-i:28;"><span class="s-flip-front">o</span><span class="s-flip-back">o</span></span><span class="s-flip-char-box" style="--char-i:29;"><span class="s-flip-front">l</span><span class="s-flip-back">l</span></span><span class="s-flip-char-box" style="--char-i:30;"><span class="s-flip-front">i</span><span class="s-flip-back">i</span></span><span class="s-flip-char-box" style="--char-i:31;"><span class="s-flip-front">s</span><span class="s-flip-back">s</span></span><span class="s-flip-char-box" style="--char-i:32;"><span class="s-flip-front">h</span><span class="s-flip-back">h</span></span><span class="s-flip-char-box" style="--char-i:33;"><span class="s-flip-front">,</span><span class="s-flip-back">,</span></span></span><span class="s-flip-space"> </span><span class="s-flip-word"><span class="s-flip-char-box" style="--char-i:34;"><span class="s-flip-front">s</span><span class="s-flip-back">s</span></span><span class="s-flip-char-box" style="--char-i:35;"><span class="s-flip-front">t</span><span class="s-flip-back">t</span></span><span class="s-flip-char-box" style="--char-i:36;"><span class="s-flip-front">a</span><span class="s-flip-back">a</span></span><span class="s-flip-char-box" style="--char-i:37;"><span class="s-flip-front">y</span><span class="s-flip-back">y</span></span></span><span class="s-flip-space"> </span><span class="s-flip-word"><span class="s-flip-char-box" style="--char-i:38;"><span class="s-flip-front">h</span><span class="s-flip-back">h</span></span><span class="s-flip-char-box" style="--char-i:39;"><span class="s-flip-front">u</span><span class="s-flip-back">u</span></span><span class="s-flip-char-box" style="--char-i:40;"><span class="s-flip-front">n</span><span class="s-flip-back">n</span></span><span class="s-flip-char-box" style="--char-i:41;"><span class="s-flip-front">g</span><span class="s-flip-back">g</span></span><span class="s-flip-char-box" style="--char-i:42;"><span class="s-flip-front">r</span><span class="s-flip-back">r</span></span><span class="s-flip-char-box" style="--char-i:43;"><span class="s-flip-front">y</span><span class="s-flip-back">y</span></span></span><span class="s-sr-only">Stay hungry, stay foolish Stay foolish, stay hungry</span></div> </h2> </div> macOS Safari browser mockup window with realistic address bar and traffic lights.
import { Safari } from "@/components/sarahui/safari" export default function SafariDemo() { return ( <div className="w-[1203px]"> <Safari url="sarahui.dev" /> </div> )}
Add Safari to your project.
$ npx sarahui add safari
import { Safari } from 'sarahui';
<Safari url="https://sarahui.dev" />
import { Safari } from "@/components/sarahui/safari"; export function SafariDemoImage() { return <Safari url="sarahui.dev" imageSrc="https://example.com/screenshot.png" />;}
import { Safari } from "@/components/sarahui/safari"; export function SafariDemoVideo() { return <Safari url="sarahui.dev" videoSrc="https://example.com/stream.mp4" />;}
import { Safari } from "@/components/sarahui/safari"; export function SafariSimple() { return <Safari mode="simple" />;}
Configuration parameters and properties for Safari.
| Prop | Type | Default | Description |
|---|---|---|---|
url |
string | - |
The URL to display in the Safari address bar |
imageSrc |
string | - |
The source URL of the image to display in the Safari window |
videoSrc |
string | - |
The source URL of the video to display in the Safari window |
mode |
SafariMode | "default" |
The display mode of the Safari window. |
className |
string | - |
Additional classes applied to the wrapper `<div> |
style |
React.CSSProperties | - |
Inline styles applied to the wrapper `<div> |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Safari({ url: "sarahui.masarax.com", children: UI.html`<div style="padding:20px;text-align:center;">Web Content</div>` }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="width:100%;max-width:540px;margin:0 auto;"><div class="s-safari-mockup s-safari--default"><div class="s-safari-header"><div class="s-safari-traffic-lights"><span class="s-safari-dot s-dot-red"></span><span class="s-safari-dot s-dot-yellow"></span><span class="s-safari-dot s-dot-green"></span></div><div class="s-safari-address-bar"><svg class="s-safari-lock" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect><path d="M7 11V7a5 5 0 0 1 10 0v4"></path></svg><span class="s-safari-url">sarahui.masarax.com</span><button type="button" class="s-safari-reload" aria-label="Reload page"><svg class="s-icon" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 12a9 9 0 0 1 9-9 9.75 9.75 0 0 1 6.74 2.74L21 8M21 3v5h-5M21 12a9 9 0 0 1-9 9 9.75 9.75 0 0 1-6.74-2.74L3 16M3 21v-5h5"/></svg></button></div><div class="s-safari-actions"><button type="button" aria-label="Share"><svg class="s-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 15 6-6M7 13l-2 2a3.5 3.5 0 0 0 5 5l3-3m-2-10 2-2a3.5 3.5 0 0 1 5 5l-3 3"/></svg></button></div></div><div class="s-safari-viewport"><div style="background:var(--s-bg-surface);padding:40px 20px;text-align:center;"><h3 style="margin:0 0 8px;">Safari Browser Mock</h3><p class="s-small s-muted">Realistic macOS window frame with native traffic lights.</p></div></div></div></div>High-fidelity iPhone device frame with dynamic island and sleek hardware bezel.
import { Iphone } from "@/components/sarahui/iphone" export default function Demo() { return ( <div className="w-[434px]"> <Iphone /> </div> )}
Add Iphone to your project.
$ npx sarahui add iphone
import { Iphone } from 'sarahui';
<Iphone />
import { Iphone } from "@/components/sarahui/iphone"; export function IphoneImage() { return <Iphone src="https://example.com/mobile-app.png" />;}
import { Iphone } from "@/components/sarahui/iphone"; export function IphoneVideo() { return <Iphone videoSrc="https://example.com/mobile-video.mp4" />;}
Configuration parameters and properties for Iphone.
| Prop | Type | Default | Description |
|---|---|---|---|
src |
string | - |
The source of the image to display |
videoSrc |
string | - |
The source of the video to display |
className |
string | - |
Additional classes applied to the wrapper `<div> |
style |
React.CSSProperties | - |
Inline styles applied to the wrapper `<div> |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Iphone({ children: UI.html`<div style="padding:20px;text-align:center;">iOS Screen</div>` }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;justify-content:center;width:100%;"><div class="s-iphone-mockup" style="--device-w:300px;--device-h:600px;"><div class="s-iphone-frame"><div class="s-iphone-speaker"></div><div class="s-iphone-dynamic-island"></div><div class="s-device-screen s-device-screen--placeholder"><div class="s-device-island"></div><div class="s-device-placeholder-content"><div class="s-device-app-bar"><span>9:41</span><div class="s-device-icons"><span>⚡️ 5G</span></div></div><div class="s-device-body-demo"><div class="s-device-card-demo"></div><div class="s-device-card-demo"></div></div></div></div><div class="s-iphone-home-bar"></div></div></div></div>Pixel-accurate Android smartphone mockup frame with punch-hole camera.
import { Android } from "@/components/sarahui/android" export default function AndroidDemo() { return ( <div className="relative"> <Android className="size-full" /> </div> )}
Add Android to your project.
$ npx sarahui add android
import { Android } from 'sarahui';
<Android />
import { Android } from "@/components/sarahui/android"; export function AndroidImage() { return <Android src="https://example.com/android-app.png" />;}
import { Android } from "@/components/sarahui/android"; export function AndroidVideo() { return <Android videoSrc="https://example.com/android-video.mp4" />;}
Configuration parameters and properties for Android.
| Prop | Type | Default | Description |
|---|---|---|---|
width |
number | 433 |
The width of the Android window |
height |
number | 882 |
The height of the Android window |
src |
string | - |
The source of the image to display |
videoSrc |
string | - |
The source of the video to display |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Android({ children: UI.html`<div style="padding:20px;text-align:center;">Android Screen</div>` }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;justify-content:center;width:100%;"><div class="s-android-mockup" style="--device-w:300px;--device-h:600px;"><div class="s-android-frame"><div class="s-android-notch"><span class="s-android-camera"></span></div><div class="s-device-screen s-device-screen--placeholder"><div class="s-device-placeholder-content"><div class="s-device-app-bar"><span>9:41</span><div class="s-device-icons"><span>●●●</span></div></div><div class="s-device-body-demo"><div class="s-device-card-demo"></div><div class="s-device-card-demo"></div></div></div></div></div></div></div>Continuous multi-color chromatic gradient button with smooth animation.
import { RainbowButton } from "@/components/sarahui/rainbow-button" export default function RainbowButtonDemo() { return <RainbowButton>Get Unlimited Access</RainbowButton>}
Add RainbowButton to your project.
$ npx sarahui add rainbow-button
import { RainbowButton } from 'sarahui';
<RainbowButton>Rainbow Button</RainbowButton>
import { RainbowButton } from "@/components/sarahui/rainbow-button"; export function RainbowButtonDemo() { return ( <RainbowButton>Access Pro Features</RainbowButton> );}
import { RainbowButton } from "@/components/sarahui/rainbow-button"; export function RainbowButtonOutline() { return ( <RainbowButton variant="outline"> Outline Chromatic Action </RainbowButton> );}
Configuration parameters and properties for RainbowButton.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
React.ReactNode | - |
The content to be displayed inside the button. |
className |
string | - |
Additional CSS classes to be applied to the button. |
variant |
"default" | "outline" | "default" |
The variant of the button. |
size |
"default" | "sm" | "lg" | "icon" | "default" |
The size of the button. |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.RainbowButton({ label: "Explore Pro Components", icon: "spark" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:220px;"><button type="button" class="s-rainbow-button"><span class="s-rainbow-button-inner"><span>Get Unlimited Access</span></span></button></div>Luminous shimmer beam gliding continuously around the button perimeter.
import { ShimmerButton } from "@/components/sarahui/shimmer-button" export default function ShimmerButtonDemo() { return ( <ShimmerButton className="shadow-2xl"> <span className="text-center text-sm leading-none font-medium tracking-tight whitespace-pre-wrap text-white lg:text-lg dark:from-white dark:to-slate-900/10"> Shimmer Button </span> </ShimmerButton> )}
Add ShimmerButton to your project.
$ npx sarahui add shimmer-button
import { ShimmerButton } from 'sarahui';
<ShimmerButton>Shimmer Button</ShimmerButton>
import { ShimmerButton } from "@/components/sarahui/shimmer-button"; export function ShimmerButtonDemo() { return <ShimmerButton label="Get Started Now" icon="spark" />;}
import { ShimmerButton } from "@/components/sarahui/shimmer-button"; export function ShimmerButtonTrailing() { return <ShimmerButton label="Continue to Docs" trailing="arrow" />;}
Configuration parameters and properties for ShimmerButton.
| Prop | Type | Default | Description |
|---|---|---|---|
shimmerColor |
string | #ffffff |
The color of the shimmer |
shimmerSize |
string | 0.05em |
The size of the shimmer |
borderRadius |
string | 100px |
The border radius of the button |
shimmerDuration |
string | 3s |
The duration of the spark animation |
background |
string | rgba(0, 0, 0, 1) |
The background of the button |
className |
string | undefined |
The class name of the button |
children |
React.ReactNode | undefined |
The children of the button |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.ShimmerButton({ label: "Get Started Now", icon: "spark" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:220px;"><button type="button" class="s-shimmer-button"><span>Shimmer Button</span></button></div>Interactive button generating expanding wave ripples from user click coordinates.
import { RippleButton } from "@/components/sarahui/ripple-button" export default function RippleButtonDemo() { return <RippleButton rippleColor="#ADD8E6">Click me</RippleButton>}
Add RippleButton to your project.
$ npx sarahui add ripple-button
import { RippleButton } from 'sarahui';
<RippleButton>Ripple Button</RippleButton>
import { RippleButton } from "@/components/sarahui/ripple-button"; export function RippleButtonDemo() { return <RippleButton>Click for Waves</RippleButton>;}
import { RippleButton } from "@/components/sarahui/ripple-button"; export function RippleButtonOutline() { return <RippleButton variant="outline" rippleColor="rgba(91,61,245,0.3)">Outline Action</RippleButton>;}
Configuration parameters and properties for RippleButton.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
React.ReactNode | - |
The content of the button. |
className |
string | - |
Additional class names for the button. |
rippleColor |
string | - |
The rbg numbers only for the color of the rippling waves. |
duration |
string | - |
The time span of one ripple. |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.RippleButton({ label: "Click for Ripple", icon: "spark" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:220px;"> <button type="button" class="s-ripple-button" data-s-ripple style="--ripple-color:rgba(255,255,255,0.4);--ripple-duration:600ms;"><span class="s-ripple-button-label">Click me</span><span class="s-ripple-surface" aria-hidden="true"></span></button> </div> High-performance interactive flickering matrix grid rendered on canvas.
import { FlickeringGrid } from "@/components/sarahui/flickering-grid" export default function FlickeringGridDemo() { return ( <div className="bg-background relative h-[500px] w-full overflow-hidden rounded-lg border"> <FlickeringGrid className="absolute inset-0 z-0 size-full" squareSize={4} gridGap={6} color="#6B7280" maxOpacity={0.5} flickerChance={0.1} height={800} width={800} /> </div> )}
Add FlickeringGrid to your project.
$ npx sarahui add flickering-grid
import { FlickeringGrid } from 'sarahui';
<FlickeringGrid />
import { FlickeringGrid } from "@/components/sarahui/flickering-grid"; export function FlickeringGridDemo() { return ( <div className="relative h-80 w-full overflow-hidden rounded-lg border"> <FlickeringGrid squareSize={4} gridGap={6} flickerChance={0.3} /> </div> );}
import { FlickeringGrid } from "@/components/sarahui/flickering-grid"; export function FlickeringGridRounded() { return <FlickeringGrid squareSize={6} gridGap={8} flickerChance={0.45} color="#00E5FF" />;}
Configuration parameters and properties for FlickeringGrid.
| Prop | Type | Default | Description |
|---|---|---|---|
squareSize |
number | 4 |
Size of each square in the grid |
gridGap |
number | 6 |
Gap between squares in the grid |
flickerChance |
number | 0.3 |
Probability of a square flickering |
color |
string | "rgb(0, 0, 0)" |
Color of the squares |
width |
number | - |
Width of the canvas |
height |
number | - |
Height of the canvas |
className |
string | - |
Additional CSS classes for the canvas |
maxOpacity |
number | 0.2 |
Maximum opacity of the squares |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.FlickeringGrid({ color: "#5B3DF5" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:380px;width:100%;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:flex;align-items:center;justify-content:center;"> <div class="s-flickering-grid" aria-hidden="true" style="--fg-color:#6B7280;--fg-opacity:0.5;"><canvas class="s-flickering-canvas" data-s-flickering data-square-size="4" data-grid-gap="6" data-flicker-chance="0.1"></canvas></div> </div> Animated matrix grid with randomly shimmering dynamic squares.
import { cn } from "sarahui/utils"import { AnimatedGridPattern } from "@/components/sarahui/animated-grid-pattern" export default function AnimatedGridPatternDemo() { return ( <div className="bg-background relative flex h-[500px] w-full items-center justify-center overflow-hidden rounded-lg border p-20"> <AnimatedGridPattern numSquares={30} maxOpacity={0.1} duration={3} repeatDelay={1} className={cn( "mask-[radial-gradient(500px_circle_at_center,white,transparent)]", "inset-x-0 inset-y-[-30%] h-[200%] skew-y-12" )} /> </div> )}
Add AnimatedGridPattern to your project.
$ npx sarahui add animated-grid-pattern
import { AnimatedGridPattern } from 'sarahui';
<AnimatedGridPattern />
import { AnimatedGridPattern } from "@/components/sarahui/animated-grid-pattern"; export function AnimatedGridPatternDemo() { return <AnimatedGridPattern numSquares={24} maxOpacity={0.5} />;}
import { AnimatedGridPattern } from "@/components/sarahui/animated-grid-pattern"; export function AnimatedGridPatternDense() { return <AnimatedGridPattern numSquares={48} maxOpacity={0.8} duration={2} />;}
Configuration parameters and properties for AnimatedGridPattern.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | - |
Additional classes to be added to the pattern |
width |
number | 40 |
Width of the pattern |
height |
number | 40 |
Height of the pattern |
x |
number | -1 |
X offset of the pattern |
y |
number | -1 |
Y offset of the pattern |
strokeDasharray |
number | 0 |
Stroke dash array of the pattern |
numSquares |
number | 200 |
Number of squares in the pattern |
maxOpacity |
number | 0.5 |
Maximum opacity of the pattern |
duration |
number | 1 |
Duration of the animation |
repeatDelay |
number | 0.5 |
Repeat delay of the animation |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.AnimatedGridPattern({ numSquares: 24 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:380px;width:100%;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:flex;align-items:center;justify-content:center;"> <svg class="s-animated-grid-pattern" aria-hidden="true"><defs><pattern id="s-animgrid-13" width="30" height="30" patternUnits="userSpaceOnUse"><path d="M 30 0 L 0 0 0 30" fill="none" stroke="currentColor" stroke-width="0.5"></path></pattern></defs><rect width="100%" height="100%" fill="url(#s-animgrid-13)"></rect><svg class="s-anim-grid-squares"><rect width="29" height="29" x="1" y="1" class="s-anim-grid-sq" style="animation-delay:0.0s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="31" y="211" class="s-anim-grid-sq" style="animation-delay:0.3s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="61" y="61" class="s-anim-grid-sq" style="animation-delay:0.6s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="91" y="271" class="s-anim-grid-sq" style="animation-delay:0.9s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="121" y="121" class="s-anim-grid-sq" style="animation-delay:1.2s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="151" y="331" class="s-anim-grid-sq" style="animation-delay:1.5s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="181" y="181" class="s-anim-grid-sq" style="animation-delay:1.8s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="211" y="31" class="s-anim-grid-sq" style="animation-delay:2.1s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="241" y="241" class="s-anim-grid-sq" style="animation-delay:2.4s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="271" y="91" class="s-anim-grid-sq" style="animation-delay:2.7s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="301" y="301" class="s-anim-grid-sq" style="animation-delay:3.0s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="331" y="151" class="s-anim-grid-sq" style="animation-delay:3.3s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="361" y="1" class="s-anim-grid-sq" style="animation-delay:3.6s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="391" y="211" class="s-anim-grid-sq" style="animation-delay:3.9s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="421" y="61" class="s-anim-grid-sq" style="animation-delay:4.2s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="451" y="271" class="s-anim-grid-sq" style="animation-delay:4.5s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="481" y="121" class="s-anim-grid-sq" style="animation-delay:4.8s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="511" y="331" class="s-anim-grid-sq" style="animation-delay:5.1s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="1" y="181" class="s-anim-grid-sq" style="animation-delay:5.4s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="31" y="31" class="s-anim-grid-sq" style="animation-delay:5.7s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="61" y="241" class="s-anim-grid-sq" style="animation-delay:6.0s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="91" y="91" class="s-anim-grid-sq" style="animation-delay:6.3s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="121" y="301" class="s-anim-grid-sq" style="animation-delay:6.6s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="151" y="151" class="s-anim-grid-sq" style="animation-delay:6.9s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="181" y="1" class="s-anim-grid-sq" style="animation-delay:7.2s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="211" y="211" class="s-anim-grid-sq" style="animation-delay:7.5s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="241" y="61" class="s-anim-grid-sq" style="animation-delay:7.8s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="271" y="271" class="s-anim-grid-sq" style="animation-delay:8.1s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="301" y="121" class="s-anim-grid-sq" style="animation-delay:8.4s;--max-opacity:0.5;"></rect><rect width="29" height="29" x="331" y="331" class="s-anim-grid-sq" style="animation-delay:8.7s;--max-opacity:0.5;"></rect></svg></svg> <p style="position:relative;z-index:2;font-size:clamp(2rem,5vw,3rem);font-weight:500;letter-spacing:-0.05em;color:var(--s-text-primary);margin:0;">Animated Grid</p> </div> Perspective 3D retro grid floor with infinite scrolling lines.
"use client" import { RetroGrid } from "@/components/sarahui/retro-grid" export default function RetroGridDemo() { return ( <div className="bg-background relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border"> <span className="pointer-events-none z-10 bg-linear-to-b from-[#ffd319] via-[#ff2975] to-[#8c1eff] bg-clip-text text-center text-7xl leading-none font-bold tracking-tighter whitespace-pre-wrap text-transparent"> Retro Grid </span> <RetroGrid /> </div> )}
Add RetroGrid to your project.
$ npx sarahui add retro-grid
import { RetroGrid } from 'sarahui';
<div className="relative h-[500px] w-full overflow-hidden"> <RetroGrid /></div>
import { RetroGrid } from "@/components/sarahui/retro-grid"; export function RetroGridDemo() { return ( <div className="relative flex h-[360px] w-full items-center justify-center overflow-hidden rounded-xl border"> <RetroGrid angle={65} /> </div> );}
import { RetroGrid } from "@/components/sarahui/retro-grid"; export function RetroGridInverted() { return <RetroGrid angle={115} opacity={0.65} />;}
Configuration parameters and properties for RetroGrid.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | - |
Additional CSS classes for the grid container |
angle |
number | 65 |
Rotation angle of the grid in degrees |
cellSize |
number | 60 |
Grid cell size in pixels |
opacity |
number | 0.5 |
Grid opacity value between 0 and 1 |
lightLineColor |
string | "gray" |
Grid line color in light mode |
darkLineColor |
string | "gray" |
Grid line color in dark mode |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.RetroGrid({ angle: 65 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:450px;width:100%;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:flex;flex-direction:column;align-items:center;justify-content:center;"> <span style="pointer-events:none;z-index:10;background:linear-gradient(to bottom, #ffd319, #ff2975, #8c1eff);-webkit-background-clip:text;-webkit-text-fill-color:transparent;text-align:center;font-size:clamp(3rem, 7vw, 5rem);line-height:1;font-weight:800;letter-spacing:-0.05em;white-space:pre-wrap;"> Retro Grid </span> <div class="s-retro-grid" aria-hidden="true"><div class="s-retro-grid-plane" style="transform:rotateX(65deg)"></div></div> </div> Concentric ambient pulsing circle waves radiating outward.
import { Ripple } from "@/components/sarahui/ripple" export default function RippleDemo() { return ( <div className="bg-background relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border"> <p className="z-10 text-center text-5xl font-medium tracking-tighter whitespace-pre-wrap text-white"> Ripple </p> <Ripple /> </div> )}
Add Ripple to your project.
$ npx sarahui add ripple
import { Ripple } from 'sarahui';
<div className="relative h-[500px] w-full overflow-hidden"> <Ripple /></div>
import { Ripple } from "@/components/sarahui/ripple"; export function RippleDemo() { return <Ripple numCircles={5} mainCircleSize={210} />;}
import { Ripple } from "@/components/sarahui/ripple"; export function RippleCompact() { return <Ripple numCircles={3} mainCircleSize={120} />;}
Configuration parameters and properties for Ripple.
| Prop | Type | Default | Description |
|---|---|---|---|
mainCircleSize |
number | 210 |
The size of the main circle in pixels |
mainCircleOpacity |
number | 0.24 |
The opacity of the main circle |
numCircles |
number | 8 |
The number of ripple circles to render |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Ripple({ numCircles: 4 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:500px;width:100%;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:grid;place-items:center;"><div class="s-ripple" aria-hidden="true"><div class="s-ripple-circle" style="width:180px;height:180px;animation-delay:0.00s;"></div><div class="s-ripple-circle" style="width:235px;height:235px;animation-delay:0.35s;"></div><div class="s-ripple-circle" style="width:290px;height:290px;animation-delay:0.70s;"></div><div class="s-ripple-circle" style="width:345px;height:345px;animation-delay:1.05s;"></div><div class="s-ripple-circle" style="width:400px;height:400px;animation-delay:1.40s;"></div><div class="s-ripple-circle" style="width:455px;height:455px;animation-delay:1.75s;"></div><div class="s-ripple-circle" style="width:510px;height:510px;animation-delay:2.10s;"></div><div class="s-ripple-circle" style="width:565px;height:565px;animation-delay:2.45s;"></div></div><p style="position:relative;z-index:2;font-size:clamp(2rem,5vw,3rem);font-weight:500;letter-spacing:-0.05em;color:#fff;margin:0;">Ripple</p></div>Dotted matrix background pattern with smooth radial falloff.
"use client" import { cn } from "sarahui/utils"import { DotPattern } from "@/components/sarahui/dot-pattern" export default function DotPatternDemo() { return ( <div className="bg-background relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border"> <DotPattern className={cn( "[mask-image:radial-gradient(300px_circle_at_center,white,transparent)]" )} /> </div> )}
Add DotPattern to your project.
$ npx sarahui add dot-pattern
import { DotPattern } from 'sarahui';
<div className="relative h-[500px] w-full overflow-hidden"> <DotPattern /></div>
import { DotPattern } from "@/components/sarahui/dot-pattern"; export function DotPatternLinearGradient() { return ( <DotPattern width={20} height={20} cr={1} className="[mask-image:linear-gradient(to_bottom_white,transparent)]" /> );}
import { DotPattern } from "@/components/sarahui/dot-pattern"; export function DotPatternGlow() { return <DotPattern glow glowColor="#5B3DF5" />;}
Configuration parameters and properties for DotPattern.
| Prop | Type | Default | Description |
|---|---|---|---|
width |
any | 16 |
Width of the dot pattern |
height |
any | 16 |
Height of the dot pattern |
x |
any | 0 |
X position of the dot |
y |
any | 0 |
Y position of the dot |
cx |
any | 1 |
X position of the circle |
cy |
any | 1 |
Y position of the circle |
cr |
any | 1 |
Radius of the circle |
className |
string | - |
Class name of the dot pattern |
glow |
boolean | false |
Activates the glow effect in the dot pattern |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.DotPattern({ width: 20, height: 20 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:380px;width:100%;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:flex;align-items:center;justify-content:center;"> <svg class="s-dot-pattern" aria-hidden="true"><defs><pattern id="s-dots-16" width="20" height="20" patternUnits="userSpaceOnUse"><circle cx="1.2" cy="1.2" r="1.2" fill="currentColor"></circle></pattern></defs><rect width="100%" height="100%" stroke-width="0" fill="url(#s-dots-16)"></rect></svg> <p style="position:relative;z-index:2;font-size:clamp(2rem,5vw,3rem);font-weight:500;letter-spacing:-0.05em;color:var(--s-text-primary);margin:0;">Dot Pattern</p> </div> Geometric SVG grid background pattern with customizable accent squares.
"use client" import { cn } from "sarahui/utils"import { GridPattern } from "@/components/sarahui/grid-pattern" export default function GridPatternDemo() { return ( <div className="bg-background relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border"> <GridPattern squares={[ [4, 4], [5, 1], [8, 2], [5, 3], [5, 5], [10, 10], [12, 15], [15, 10], [10, 15], [15, 10], [10, 15], [15, 10], ]} className={cn( "[mask-image:radial-gradient(400px_circle_at_center,white,transparent)]", "inset-x-0 inset-y-[-30%] h-[200%] skew-y-12" )} /> </div> )}
Add GridPattern to your project.
$ npx sarahui add grid-pattern
import { GridPattern } from 'sarahui';
<div className="relative h-[500px] w-full overflow-hidden"> <GridPattern /></div>
import { GridPattern } from "@/components/sarahui/grid-pattern"; export function GridPatternLinear() { return ( <GridPattern width={40} height={40} className="[mask-image:linear-gradient(to_bottom_white,transparent)]" /> );}
import { GridPattern } from "@/components/sarahui/grid-pattern"; export function GridPatternDashed() { return ( <GridPattern width={32} height={32} strokeDasharray="4 2" /> );}
Configuration parameters and properties for GridPattern.
| Prop | Type | Default | Description |
|---|---|---|---|
width |
number | 40 |
Width of the pattern |
height |
number | 40 |
Height of the pattern |
x |
number | -1 |
X offset of the pattern |
y |
number | -1 |
Y offset of the pattern |
squares |
number | [] |
X Y coordinates of filled squares as 2D array |
strokeDasharray |
string | 0 |
Stroke dash array for the pattern |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.GridPattern({ width: 40, height: 40 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:380px;width:100%;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:flex;align-items:center;justify-content:center;"> <svg class="s-grid-pattern" aria-hidden="true"><defs><pattern id="s-grid-19" width="40" height="40" patternUnits="userSpaceOnUse" x="0" y="0"><path d="M.5 40V.5H40" fill="none" stroke="currentColor" stroke-dasharray="0"></path></pattern></defs><rect width="100%" height="100%" stroke-width="0" fill="url(#s-grid-19)"></rect><svg x="0" y="0" class="s-grid-squares"><rect x="161" y="161" width="39" height="39" class="s-grid-fill"/><rect x="201" y="41" width="39" height="39" class="s-grid-fill"/><rect x="321" y="81" width="39" height="39" class="s-grid-fill"/><rect x="201" y="121" width="39" height="39" class="s-grid-fill"/><rect x="201" y="201" width="39" height="39" class="s-grid-fill"/><rect x="401" y="401" width="39" height="39" class="s-grid-fill"/><rect x="481" y="601" width="39" height="39" class="s-grid-fill"/><rect x="601" y="401" width="39" height="39" class="s-grid-fill"/><rect x="401" y="601" width="39" height="39" class="s-grid-fill"/><rect x="601" y="401" width="39" height="39" class="s-grid-fill"/><rect x="401" y="601" width="39" height="39" class="s-grid-fill"/><rect x="601" y="401" width="39" height="39" class="s-grid-fill"/></svg></svg> <p style="position:relative;z-index:2;font-size:clamp(2rem,5vw,3rem);font-weight:500;letter-spacing:-0.05em;color:var(--s-text-primary);margin:0;">Grid Pattern</p> </div> Geometric honeycomb hexagonal tessellation vector background pattern.
"use client" import { cn } from "sarahui/utils"import { HexagonPattern } from "@/components/sarahui/hexagon-pattern" export default function HexagonPatternDemo() { return ( <div className="bg-background relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border"> <HexagonPattern hexagons={[ [1, 1], [4, 4], [2, 2], [3, 4], [5, 4], [8, 2], [6, 3], [8, 5], [10, 10], ]} className={cn( "mask-[radial-gradient(420px_circle_at_center,white,transparent)]", "inset-0 skew-y-6" )} /> </div> )}
Add HexagonPattern to your project.
$ npx sarahui add hexagon-pattern
import { HexagonPattern } from 'sarahui';
<div className="relative h-[500px] w-full overflow-hidden"> <HexagonPattern /></div>
import { HexagonPattern } from "@/components/sarahui/hexagon-pattern"; export function HexagonPatternLinear() { return ( <HexagonPattern size={36} className="[mask-image:linear-gradient(to_bottom_white,transparent)]" /> );}
import { HexagonPattern } from "@/components/sarahui/hexagon-pattern"; export function HexagonPatternDashed() { return ( <HexagonPattern size={32} strokeDasharray="3 3" /> );}
Configuration parameters and properties for HexagonPattern.
| Prop | Type | Default | Description |
|---|---|---|---|
radius |
number | 40 |
Radius from center to vertex for each hexagon |
gap |
number | 0 |
Extra spacing between adjacent hexagons (pixels) |
x |
number | -1 |
X offset of the pattern origin |
y |
number | -1 |
Y offset of the pattern origin |
direction |
"horizontal" | "vertical" | "horizontal" |
horizontal` — flat-top honeycomb; `vertical` — pointy-top honeycomb |
hexagons |
Array<[col, row]> | — |
Grid coordinates of highlighted (filled) hexagons |
strokeDasharray |
string | "0" |
SVG `stroke-dasharray` for outline hexagons in the repeating pattern |
className |
string | — |
Additional classes on the root SVG (for example Tailwind color utilities) |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.HexagonPattern({ size: 36 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:180px;width:100%;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:grid;place-items:center;"><svg class="s-hexagon-pattern" aria-hidden="true"><defs><pattern id="s-hex-22" width="96.9" height="56.0" patternUnits="userSpaceOnUse"><polygon class="s-hex-poly" points="54.2,28.0 39.2,54.0 9.2,54.0 -5.8,28.0 9.2,2.0 39.2,2.0" stroke-dasharray="0"/><polygon class="s-hex-poly" points="102.7,56.0 87.7,81.9 57.7,81.9 42.7,56.0 57.7,30.0 87.7,30.0" stroke-dasharray="0"/></pattern></defs><rect width="100%" height="100%" fill="url(#s-hex-22)"/><svg class="s-hex-highlighted"><polygon class="s-hex-fill" points="101.7,83.9 87.2,109.1 58.2,109.1 43.7,83.9 58.2,58.8 87.2,58.8"/><polygon class="s-hex-fill" points="198.6,139.9 184.1,165.0 155.1,165.0 140.6,139.9 155.1,114.8 184.1,114.8"/></svg></svg><span style="position:relative;z-index:2;font-weight:600;font-size:13px;">Honeycomb Hexagon Pattern</span></div>Customizable angled linear striped pattern background.
import { StripedPattern } from "@/components/sarahui/striped-pattern" export default function StripedPatternDemo() { return ( <div className="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border"> <StripedPattern className="[mask-image:radial-gradient(300px_circle_at_center,white,transparent)]" /> </div> )}
Add StripedPattern to your project.
$ npx sarahui add striped-pattern
import { StripedPattern } from 'sarahui';
<div className="relative h-[500px] w-full overflow-hidden"> <StripedPattern /></div>
import { StripedPattern } from "@/components/sarahui/striped-pattern"; export function StripedPatternDashed() { return ( <StripedPattern angle={45} width={24} height={24} strokeDasharray="4 4" /> );}
import { StripedPattern } from "@/components/sarahui/striped-pattern"; export function StripedPatternHorizontal() { return ( <StripedPattern direction="right" width={16} height={16} /> );}
Configuration parameters and properties for StripedPattern.
| Prop | Type | Default | Description |
|---|---|---|---|
width |
number | 40 |
Width of the pattern |
height |
number | 40 |
Height of the pattern |
x |
number | -1 |
X offset of the pattern |
y |
number | -1 |
Y offset of the pattern |
direction |
"left"` | `"right" | "left" |
Line direction |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.StripedPattern({ angle: 45 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:180px;width:100%;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:grid;place-items:center;"><svg class="s-striped-pattern" aria-hidden="true"><defs><pattern id="s-stripe-25" width="24" height="24" patternUnits="userSpaceOnUse"><line x1="0" y1="24" x2="24" y2="0" stroke="currentColor"/><line x1="-24" y1="24" x2="0" y2="0" stroke="currentColor"/><line x1="24" y1="24" x2="48" y2="0" stroke="currentColor"/></pattern></defs><rect width="100%" height="100%" fill="url(#s-stripe-25)"/></svg><span style="position:relative;z-index:2;font-weight:600;font-size:13px;">Diagonal Striped Pattern</span></div>Interactive vector grid pattern where individual cells illuminate on hover.
"use client" import { cn } from "sarahui/utils"import { InteractiveGridPattern } from "@/components/sarahui/interactive-grid-pattern" export default function InteractiveGridPatternDemo() { return ( <div className="bg-background relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border"> <InteractiveGridPattern className={cn( "[mask-image:radial-gradient(400px_circle_at_center,white,transparent)]", "inset-x-0 inset-y-[-30%] h-[200%] skew-y-12" )} /> </div> )}
Add InteractiveGridPattern to your project.
$ npx sarahui add interactive-grid-pattern
import { InteractiveGridPattern } from 'sarahui';
<div className="relative h-[500px] w-full overflow-hidden"> <InteractiveGridPattern /></div>
import { InteractiveGridPattern } from "@/components/sarahui/interactive-grid-pattern"; export function InteractiveGridPatternDemo() { return ( <div className="relative h-80 w-full overflow-hidden rounded-xl border"> <InteractiveGridPattern width={36} height={36} numSquares={24} /> </div> );}
import { InteractiveGridPattern } from "@/components/sarahui/interactive-grid-pattern"; export function InteractiveGridPatternColorful() { return <InteractiveGridPattern width={24} height={24} numSquares={48} strokeDasharray="4 4" />;}
Configuration parameters and properties for InteractiveGridPattern.
| Prop | Type | Default | Description |
|---|---|---|---|
width |
number | 40 |
Width of each square in the grid |
height |
number | 40 |
Height of each square in the grid |
squares |
[number, number] | [24,24] |
Number of squares in the grid. First number is horizontal squares, second is vertical squares |
className |
string | - |
Class name applied to the grid container |
squaresClassName |
string | - |
Class name applied to individual squares in the grid |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.InteractiveGridPattern({ width: 32, height: 32 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:180px;width:100%;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:grid;place-items:center;"><svg class="s-interactive-grid-pattern" data-s-interactive-grid><rect x="0" y="0" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="0"/><rect x="32" y="0" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="1"/><rect x="64" y="0" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="2"/><rect x="96" y="0" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="3"/><rect x="128" y="0" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="4"/><rect x="160" y="0" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="5"/><rect x="192" y="0" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="6"/><rect x="224" y="0" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="7"/><rect x="256" y="0" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="8"/><rect x="288" y="0" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="9"/><rect x="320" y="0" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="10"/><rect x="352" y="0" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="11"/><rect x="0" y="32" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="12"/><rect x="32" y="32" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="13"/><rect x="64" y="32" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="14"/><rect x="96" y="32" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="15"/><rect x="128" y="32" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="16"/><rect x="160" y="32" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="17"/><rect x="192" y="32" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="18"/><rect x="224" y="32" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="19"/><rect x="256" y="32" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="20"/><rect x="288" y="32" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="21"/><rect x="320" y="32" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="22"/><rect x="352" y="32" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="23"/><rect x="0" y="64" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="24"/><rect x="32" y="64" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="25"/><rect x="64" y="64" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="26"/><rect x="96" y="64" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="27"/><rect x="128" y="64" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="28"/><rect x="160" y="64" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="29"/><rect x="192" y="64" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="30"/><rect x="224" y="64" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="31"/><rect x="256" y="64" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="32"/><rect x="288" y="64" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="33"/><rect x="320" y="64" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="34"/><rect x="352" y="64" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="35"/><rect x="0" y="96" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="36"/><rect x="32" y="96" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="37"/><rect x="64" y="96" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="38"/><rect x="96" y="96" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="39"/><rect x="128" y="96" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="40"/><rect x="160" y="96" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="41"/><rect x="192" y="96" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="42"/><rect x="224" y="96" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="43"/><rect x="256" y="96" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="44"/><rect x="288" y="96" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="45"/><rect x="320" y="96" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="46"/><rect x="352" y="96" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="47"/><rect x="0" y="128" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="48"/><rect x="32" y="128" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="49"/><rect x="64" y="128" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="50"/><rect x="96" y="128" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="51"/><rect x="128" y="128" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="52"/><rect x="160" y="128" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="53"/><rect x="192" y="128" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="54"/><rect x="224" y="128" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="55"/><rect x="256" y="128" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="56"/><rect x="288" y="128" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="57"/><rect x="320" y="128" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="58"/><rect x="352" y="128" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="59"/><rect x="0" y="160" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="60"/><rect x="32" y="160" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="61"/><rect x="64" y="160" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="62"/><rect x="96" y="160" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="63"/><rect x="128" y="160" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="64"/><rect x="160" y="160" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="65"/><rect x="192" y="160" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="66"/><rect x="224" y="160" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="67"/><rect x="256" y="160" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="68"/><rect x="288" y="160" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="69"/><rect x="320" y="160" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="70"/><rect x="352" y="160" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="71"/><rect x="0" y="192" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="72"/><rect x="32" y="192" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="73"/><rect x="64" y="192" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="74"/><rect x="96" y="192" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="75"/><rect x="128" y="192" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="76"/><rect x="160" y="192" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="77"/><rect x="192" y="192" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="78"/><rect x="224" y="192" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="79"/><rect x="256" y="192" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="80"/><rect x="288" y="192" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="81"/><rect x="320" y="192" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="82"/><rect x="352" y="192" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="83"/><rect x="0" y="224" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="84"/><rect x="32" y="224" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="85"/><rect x="64" y="224" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="86"/><rect x="96" y="224" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="87"/><rect x="128" y="224" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="88"/><rect x="160" y="224" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="89"/><rect x="192" y="224" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="90"/><rect x="224" y="224" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="91"/><rect x="256" y="224" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="92"/><rect x="288" y="224" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="93"/><rect x="320" y="224" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="94"/><rect x="352" y="224" width="32" height="32" class="s-interactive-grid-rect" data-grid-i="95"/></svg><span style="position:relative;z-index:2;font-weight:600;font-size:13px;">Interactive Hover Grid Cells</span></div>Volumetric atmospheric god rays oscillating smoothly across backgrounds.
import { LightRays } from "@/components/sarahui/light-rays" export default function Component() { return ( <div className="relative h-[500px] w-full overflow-hidden rounded-lg border"> <div className="relative z-10 flex h-full flex-col items-center justify-center gap-4 px-6 text-center"> <span className="text-xs font-semibold tracking-[0.35em] text-slate-800/60 uppercase dark:text-slate-200/60"> Ambient glow </span> <h1 className="text-foreground text-4xl font-bold md:text-5xl"> Light Rays </h1> <p className="max-w-md text-sm text-slate-800/80 md:text-base dark:text-slate-200/80"> Drop this component into any container and it will fill the space with softly animated light rays shining from above. </p> </div> <LightRays /> </div> )}
Add LightRays to your project.
$ npx sarahui add light-rays
import { LightRays } from 'sarahui';
<div className="relative h-[400px] w-full overflow-hidden rounded-xl border"> <LightRays /></div>
import { LightRays } from "@/components/sarahui/light-rays"; export function LightRaysDemo() { return ( <div className="relative h-80 w-full overflow-hidden rounded-xl border bg-black"> <LightRays intensity={0.7} count={5} /> </div> );}
import { LightRays } from "@/components/sarahui/light-rays"; export function LightRaysAmbient() { return <LightRays intensity={0.4} count={8} speed={12} />;}
Configuration parameters and properties for LightRays.
| Prop | Type | Default | Description |
|---|---|---|---|
count |
number | 7 |
Total number of animated rays. |
color |
string | "rgba(160, 210, 255, 0.2)" |
Base colour used for the gradients. |
blur |
number | 36 |
Pixel radius applied to the blur filter. |
opacity |
number | 0.65 |
Maximum opacity that rays reach (0 – 1). |
speed |
number | 14 |
Average seconds each ray takes to complete a cycle. |
length |
string | number | "70vh" |
CSS length for the ray height. |
className |
string | undefined |
Additional class names applied to the wrapper. |
style |
React.CSSProperties | undefined |
Inline styles merged into the wrapper. |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.LightRays({ count: 6, intensity: 0.5 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:200px;width:100%;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:grid;place-items:center;"><div class="s-light-rays" style="--light-rays-color:rgba(160, 210, 255, 0.2);--light-rays-blur:36px;--light-rays-length:70vh;" aria-hidden="true"><div class="s-light-rays-glow"></div><div class="s-light-ray" style="--ray-left:10%;--ray-rot:-25deg;--ray-width:140px;--ray-dur:11.2s;--ray-delay:0.0s;"></div><div class="s-light-ray" style="--ray-left:13%;--ray-rot:-8deg;--ray-width:169px;--ray-dur:12.6s;--ray-delay:0.7s;"></div><div class="s-light-ray" style="--ray-left:16%;--ray-rot:9deg;--ray-width:198px;--ray-dur:14.0s;--ray-delay:1.4s;"></div><div class="s-light-ray" style="--ray-left:19%;--ray-rot:-24deg;--ray-width:227px;--ray-dur:15.4s;--ray-delay:2.1s;"></div><div class="s-light-ray" style="--ray-left:22%;--ray-rot:-7deg;--ray-width:256px;--ray-dur:16.8s;--ray-delay:2.8s;"></div><div class="s-light-ray" style="--ray-left:25%;--ray-rot:10deg;--ray-width:145px;--ray-dur:11.2s;--ray-delay:3.5s;"></div><div class="s-light-ray" style="--ray-left:28%;--ray-rot:-23deg;--ray-width:174px;--ray-dur:12.6s;--ray-delay:4.2s;"></div></div><span style="position:relative;z-index:2;font-weight:600;font-size:13px;">God Rays Volumetric Lighting</span></div>Tactile SVG fractal noise texture overlay adding physical depth to surfaces.
"use client" import { cn } from "sarahui/utils"import { NoiseTexture } from "@/components/sarahui/noise-texture" export default function NoiseTextureDemo() { return ( <div className="relative flex h-[500px] w-full flex-col items-center justify-center overflow-hidden rounded-lg border bg-neutral-100/80 dark:bg-neutral-950"> <NoiseTexture className={cn( "absolute inset-0", "mask-[radial-gradient(420px_circle_at_center,white,transparent)]" )} /> </div> )}
Add NoiseTexture to your project.
$ npx sarahui add noise-texture
import { NoiseTexture } from 'sarahui';
<div className="relative h-64 overflow-hidden rounded-lg border"> <NoiseTexture /></div>
import { NoiseTexture } from "@/components/sarahui/noise-texture"; export function NoiseTextureDigest() { return ( <div className="relative p-6 border rounded-xl overflow-hidden"> <NoiseTexture opacity={0.25} /> <h3>Subscribe to Newsletter</h3> </div> );}
import { NoiseTexture } from "@/components/sarahui/noise-texture"; export function NoiseTextureButton() { return ( <button className="relative overflow-hidden btn-primary"> <NoiseTexture opacity={0.3} /> <span>Tactile Button</span> </button> );}
Configuration parameters and properties for NoiseTexture.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | — |
Merged onto the root `svg` (via `cn`) |
frequency |
number | 0.4 |
baseFrequency` for `feTurbulence |
octaves |
number | 6 |
numOctaves` for `feTurbulence |
slope |
number | 0.15 |
Linear slope per channel after desaturation |
noiseOpacity |
number | 0.6 |
Opacity of the noise `rect |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.NoiseTexture({ opacity: 0.15 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:180px;width:100%;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:linear-gradient(135deg,#5B3DF5,#178BFF);display:grid;place-items:center;color:#fff;"><svg class="s-noise-texture" aria-hidden="true"><defs><filter id="s-noise-28"><feTurbulence type="fractalNoise" baseFrequency="0.4" numOctaves="5" stitchTiles="stitch"/><feColorMatrix type="saturate" values="0"/><feComponentTransfer><feFuncR type="linear" slope="0.15"/><feFuncG type="linear" slope="0.15"/><feFuncB type="linear" slope="0.15"/></feComponentTransfer></filter></defs><rect width="100%" height="100%" filter="url(#s-noise-28)" opacity="0.6"/></svg><span style="position:relative;z-index:2;font-weight:700;font-size:14px;">Subtle Noise Texture Filter</span></div>Depth-projected 3D perspective particle canvas with smooth orbital drift.
import { ArrowRightIcon } from "sarahui/icons";import { Floating3DParticles } from "@/components/sarahui/floating-3d-particles"; export function Floating3DParticlesDemo() { const color = "var(--s-brand-indigo, #5B3DF5)"; return ( <div className="bg-background relative flex h-[500px] w-full items-center justify-center overflow-hidden rounded-lg border"> <Floating3DParticles color={color} /> <div className="relative z-10 flex flex-col items-center gap-6 px-6 text-center"> <h2 className="text-3xl font-bold tracking-tight md:text-5xl"> Build something magical </h2> <p className="text-muted-foreground max-w-xl text-base md:text-lg"> A pseudo-3D particle background that stays behind your content with continuous rotation. </p> </div> </div> );}
Add Floating3DParticles to your project.
$ npx sarahui add floating3-dparticles
import { Floating3DParticles } from 'sarahui';
<div className="relative flex h-[500px] w-full items-center justify-center overflow-hidden rounded-lg border"> <Floating3DParticles color="#8B5CF6" quantity={400} /> <div className="relative z-10 text-center"> <h2 className="text-3xl font-bold">Build Something Magical</h2> </div></div>
import { Floating3DParticles } from "@/components/sarahui/floating-3d-particles"; export function Floating3DParticlesDemo() { return ( <div className="relative h-80 w-full overflow-hidden rounded-xl border"> <Floating3DParticles count={40} size={3} /> </div> );}
import { Floating3DParticles } from "@/components/sarahui/floating-3d-particles"; export function Floating3DParticlesCyan() { return <Floating3DParticles count={60} size={2} color="#00E5FF" />;}
Configuration parameters and properties for Floating3DParticles.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | - |
Additional CSS classes for the canvas element. |
quantity |
number | 400 |
Number of particles on desktop viewports. Automatically scales down to 20% on mobile screens (< 768px). |
color |
string | "#8B5CF6" |
Particle color (supports 3-digit or 6-digit hex). |
size |
number | 5 |
Mean particle radius in px. Each particle varies within ±40% of this value. |
opacity |
number | 0.3 |
Mean particle opacity (0–1). Each particle varies within ±0.2, clamped to [0, 1]. |
drift |
number | 0.8 |
Vertical floating speed in px per frame. Positive drifts upward, negative downward. |
depth |
number | 0.5 |
3D depth intensity (0–1). `0` = flat 2D plane; `1` = strong perspective with pronounced near/far scaling. |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Floating3DParticles({ count: 40 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:200px;width:100%;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:grid;place-items:center;"><div class="s-floating-3d-particles" aria-hidden="true"><canvas class="s-floating-particles-canvas" data-s-floating-particles data-quantity="120" data-color="#8B5CF6" data-size="4" data-opacity="0.35" data-drift="0.8" data-depth="0.5"></canvas></div><span style="position:relative;z-index:2;font-weight:600;font-size:13px;">3D Perspective Floating Particles</span></div>Action button with continuous luminous sheen pass sweeping across its surface.
import { ShinyButton } from "@/components/sarahui/shiny-button" export default function ShinyButtonDemo() { return <ShinyButton>Shiny Button</ShinyButton>}
Add ShinyButton to your project.
$ npx sarahui add shiny-button
import { ShinyButton } from 'sarahui';
<ShinyButton>Shiny Button</ShinyButton>
import { ShinyButton } from "@/components/sarahui/shiny-button"; export function ShinyButtonDemo() { return <ShinyButton label="Get Unlimited Access" />;}
import { ShinyButton } from "@/components/sarahui/shiny-button"; export function ShinyButtonCompact() { return <ShinyButton size="sm" label="Quick Action" />;}
Configuration parameters and properties for ShinyButton.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | - |
The class name to be applied to the component |
children |
React.ReactNode | - |
The content to be displayed inside the button |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.ShinyButton({ label: "Get Pro Access", icon: "spark" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:220px;"> <button type="button" class="s-shiny-button"><span class="s-shiny-button-content"><span>Shiny Button</span></span><span class="s-shiny-button-sweep" aria-hidden="true"></span></button> </div> Hierarchical file tree with folder disclosure and file icon mapping.
import { Tree } from "@/components/sarahui/file-tree"import type { TreeViewElement } from "@/components/sarahui/file-tree" export default function FileTreeDemo() { return ( <div className="bg-background relative flex h-[300px] w-full max-w-sm flex-col items-center justify-center overflow-hidden rounded-lg border"> <Tree className="bg-background overflow-hidden rounded-md p-2" initialSelectedId="button" initialExpandedItems={["src", "app", "components", "ui", "lib"]} elements={ELEMENTS} /> </div> )} const ELEMENTS: TreeViewElement[] = [ { id: "src", type: "folder", isSelectable: true, name: "src", children: [ { id: "lib", type: "folder", isSelectable: true, name: "lib", children: [ { id: "utils", isSelectable: true, name: "utils.ts", }, ], }, { id: "app", type: "folder", isSelectable: true, name: "app", children: [ { id: "page", isSelectable: true, name: "page.tsx", }, { id: "layout", isSelectable: true, name: "layout.tsx", }, ], }, { id: "components", type: "folder", isSelectable: true, name: "components", children: [ { id: "header", isSelectable: true, name: "header.tsx", }, { id: "ui", type: "folder", isSelectable: true, name: "ui", children: [ { id: "button", isSelectable: true, name: "button.tsx", }, ], }, { id: "footer", isSelectable: true, name: "footer.tsx", }, ], }, ], },]
Add FileTree to your project.
$ npx sarahui add file-tree
import { FileTree } from 'sarahui';
const elements: TreeViewElement[] = [ { id: "src", type: "folder", name: "src", children: [ { id: "components", type: "folder", name: "components" }, { id: "app", type: "folder", name: "app" }, { id: "page", name: "page.tsx" }, { id: "layout", name: "layout.tsx" }, ], },] <FileTree tree={elements} />
import { FileTree, Folder, File } from "@/components/sarahui/file-tree"; export function FileTreeDemo() { return ( <FileTree> <Folder name="src"> <Folder name="components"> <File name="button.tsx" /> <File name="dialog.tsx" /> </Folder> <File name="index.ts" /> <File name="styles.css" /> </Folder> </FileTree> );}
import { FileTree, Folder, File } from "@/components/sarahui/file-tree"; export function FileTreeNested() { return ( <FileTree> <Folder name="apps/web"> <Folder name="app/(dashboard)"> <File name="page.tsx" /> <File name="layout.tsx" /> </Folder> <File name="next.config.mjs" /> <File name="package.json" /> </Folder> </FileTree> );}
Configuration parameters and properties for FileTree.
| Prop | Type | Default | Description |
|---|---|---|---|
initialSelectedId |
string | - |
The ID of the initially selected item. |
indicator |
boolean | true |
Whether to show the tree indicator line. |
elements |
TreeViewElement[] | - |
An array of tree view elements to render when `children` are omitted. |
initialExpandedItems |
string[] | - |
An array of IDs for items that should be initially expanded. |
openIcon |
React.ReactNode | - |
Custom icon for open folders. |
closeIcon |
React.ReactNode | - |
Custom icon for closed folders. |
sort |
"default" | "none" | ((a, b) => number) | "default" |
Sorting mode used for data-driven trees rendered from `elements`. |
dir |
"rtl" | "ltr" | "ltr" |
The text direction of the tree. |
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | - |
A unique identifier for the node. |
name |
string | - |
The label rendered for the node in data-driven mode. |
type |
"file" | "folder" | - |
Explicit node type. Use `"folder"` for empty folders. |
isSelectable |
boolean | true |
Whether the node can be selected. |
children |
TreeViewElement[] | - |
Child nodes for folders. |
| Prop | Type | Default | Description |
|---|---|---|---|
element |
string | - |
The name of the folder. |
value |
string | - |
The unique identifier for the folder. |
isSelectable |
boolean | true |
Whether the folder can be selected. |
isSelect |
boolean | - |
Whether the folder is currently selected. |
| Prop | Type | Default | Description |
|---|---|---|---|
value |
string | - |
The unique identifier for the file. |
isSelectable |
boolean | true |
Whether the file can be selected. |
isSelect |
boolean | - |
Whether the file is currently selected. |
fileIcon |
React.ReactNode | - |
Custom icon for the file. |
| Prop | Type | Default | Description |
|---|---|---|---|
elements |
TreeViewElement[] | - |
An array of tree view elements to control. |
expandAll |
boolean | false |
Whether to expand all elements initially. |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.FileTree({ tree: [{ name: "src", type: "folder", children: [{ name: "index.js", type: "file" }] }] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="max-width:340px;width:100%;margin:0 auto;"> <div class="s-file-tree" data-s-file-tree><div class="s-file-tree-actions"><button type="button" class="s-tree-toggle-all" data-s-tree-toggle-all><span>Expand / Collapse All</span></button></div><div class="s-file-tree-content" role="tree"><div class="s-tree-folder s-tree-folder--open" data-tree-id="src"><button type="button" class="s-tree-trigger" data-s-tree-folder><span class="s-tree-icon s-folder-icon"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h7l2 3h9v12H3Zm0 0V3h7l2 3h9v3"/></svg></span><span class="s-tree-name">src</span></button><div class="s-tree-children" role="group"><div class="s-tree-folder s-tree-folder--open" data-tree-id="app"><button type="button" class="s-tree-trigger" data-s-tree-folder><span class="s-tree-icon s-folder-icon"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h7l2 3h9v12H3Zm0 0V3h7l2 3h9v3"/></svg></span><span class="s-tree-name">app</span></button><div class="s-tree-children" role="group"><div class="s-tree-file" data-tree-id="layout"><button type="button" class="s-tree-trigger" data-s-tree-file><span class="s-tree-icon s-file-icon"><svg class="s-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 6-6 6 6 6m8-12 6 6-6 6M14 3l-4 18"/></svg></span><span class="s-tree-name">layout.tsx</span></button></div><div class="s-tree-file" data-tree-id="page"><button type="button" class="s-tree-trigger" data-s-tree-file><span class="s-tree-icon s-file-icon"><svg class="s-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 6-6 6 6 6m8-12 6 6-6 6M14 3l-4 18"/></svg></span><span class="s-tree-name">page.tsx</span></button></div></div></div><div class="s-tree-folder s-tree-folder--open" data-tree-id="components"><button type="button" class="s-tree-trigger" data-s-tree-folder><span class="s-tree-icon s-folder-icon"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h7l2 3h9v12H3Zm0 0V3h7l2 3h9v3"/></svg></span><span class="s-tree-name">components</span></button><div class="s-tree-children" role="group"><div class="s-tree-folder s-tree-folder--open" data-tree-id="ui"><button type="button" class="s-tree-trigger" data-s-tree-folder><span class="s-tree-icon s-folder-icon"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h7l2 3h9v12H3Zm0 0V3h7l2 3h9v3"/></svg></span><span class="s-tree-name">ui</span></button><div class="s-tree-children" role="group"><div class="s-tree-file" data-tree-id="button"><button type="button" class="s-tree-trigger" data-s-tree-file><span class="s-tree-icon s-file-icon"><svg class="s-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 6-6 6 6 6m8-12 6 6-6 6M14 3l-4 18"/></svg></span><span class="s-tree-name">button.tsx</span></button></div></div></div><div class="s-tree-file" data-tree-id="footer"><button type="button" class="s-tree-trigger" data-s-tree-file><span class="s-tree-icon s-file-icon"><svg class="s-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 6-6 6 6 6m8-12 6 6-6 6M14 3l-4 18"/></svg></span><span class="s-tree-name">footer.tsx</span></button></div><div class="s-tree-file" data-tree-id="header"><button type="button" class="s-tree-trigger" data-s-tree-file><span class="s-tree-icon s-file-icon"><svg class="s-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 6-6 6 6 6m8-12 6 6-6 6M14 3l-4 18"/></svg></span><span class="s-tree-name">header.tsx</span></button></div></div></div><div class="s-tree-folder s-tree-folder--open" data-tree-id="lib"><button type="button" class="s-tree-trigger" data-s-tree-folder><span class="s-tree-icon s-folder-icon"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h7l2 3h9v12H3Zm0 0V3h7l2 3h9v3"/></svg></span><span class="s-tree-name">lib</span></button><div class="s-tree-children" role="group"><div class="s-tree-file" data-tree-id="utils"><button type="button" class="s-tree-trigger" data-s-tree-file><span class="s-tree-icon s-file-icon"><svg class="s-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 6-6 6 6 6m8-12 6 6-6 6M14 3l-4 18"/></svg></span><span class="s-tree-name">utils.ts</span></button></div></div></div></div></div></div></div> </div> Side-by-side or stacked code comparison container with syntax layout.
import { NextRequest } from 'next/server';
export const middleware = async (req: NextRequest) => {
let user = undefined;
let team = undefined;
const token = req.headers.get('token');
if(req.nextUrl.pathname.startsWith('/auth')) {
user = await getUserByToken(token);
if(!user) {
return NextResponse.redirect('/login');
}
}
if(req.nextUrl.pathname.startsWith('/team')) {
user = await getUserByToken(token);
if(!user) {
return NextResponse.redirect('/login');
}
const slug = req.nextUrl.query.slug;
team = await getTeamBySlug(slug);
if(!team) {
return NextResponse.redirect('/');
}
}
return NextResponse.next();
}
export const config = {
matcher: ['/((?!_next/|_static|_vercel|[\w-]+\.\w+).*)'],
};import { createMiddleware } from '@app/(auth)/auth/_middleware';
import { auth } from '@/app/(auth)/auth/_middleware';
import { team } from '@/app/(team)/team/_middleware';
const middlewares = {
'/auth{/:path?}': auth,
'/team{/:slug?}': [ auth, team ],
};
export const middleware = createMiddleware(middlewares);
export const config = {
matcher: ['/((?!_next/|_static|_vercel|[\w-]+\.\w+).*)'],
};import { CodeComparison } from "@/components/sarahui/code-comparison"; const beforeCode = `// Standard Request Routerexport async function handleRequest(req: Request) { const token = req.headers.get('authorization'); const user = await verifyAuth(token); if (!user) { return new Response('Unauthorized', { status: 401 }); } const data = await fetchUserData(user.id); return Response.json(data);}`; const afterCode = `// SarahUI Enhanced Pipelineimport { createPipeline } from 'sarahui/pipeline'; export const handleRequest = createPipeline({ auth: verifyAuth, handler: async ({ user }) => { const data = await fetchUserData(user.id); return Response.json(data); }});`; export function CodeComparisonDemo() { return ( <CodeComparison beforeCode={beforeCode} afterCode={afterCode} language="typescript" filename="router.ts" lightTheme="github-light" darkTheme="github-dark" /> );}
Add CodeComparison to your project.
$ npx sarahui add code-comparison
import { CodeComparison } from 'sarahui';
<CodeComparison beforeCode={beforeCode} afterCode={afterCode} />
const legacy = true;
const slow = true;import { Button } from 'sarahui';
// Fast & zero-deps!import { CodeComparison } from "@/components/sarahui/code-comparison"; export function CodeComparisonDemo() { return ( <CodeComparison beforeCode="const legacy = true;" afterCode="import { Button } from 'sarahui';" beforeTitle="Legacy Code" afterTitle="sarahUI" /> );}
function greet(user) {
return 'Hello, ' + user.name;
}interface User { name: string; }
function greet(user: User): string {
return `Hello, ${user.name}`;
}import { CodeComparison } from "@/components/sarahui/code-comparison"; export function CodeComparisonTypes() { return ( <CodeComparison beforeCode="function greet(user) {}" afterCode="function greet(user: User): string {}" beforeTitle="JavaScript" afterTitle="TypeScript" /> );}
Configuration parameters and properties for CodeComparison.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | - |
The class name to be applied to the component |
beforeCode |
string | - |
The code snippet to display in the "before" section |
afterCode |
string | - |
The code snippet to display in the "after" section |
language |
string | - |
The language of the code snippets (e.g., "typescript") |
filename |
string | - |
The filename to display for the code snippets |
lightTheme |
string | github-light |
The theme to use for light mode |
darkTheme |
string | github-dark |
The theme to use for dark mode |
highlightColor |
string | rgba(101, 117, 133, 0.16) |
The color to use for highlighting the code snippets |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.CodeComparison({ beforeCode: "const a = 1;", afterCode: "const b = 2;", beforeTitle: "Before", afterTitle: "After" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="width:100%;max-width:760px;margin:0 auto;"> <div class="s-code-comparison"><div class="s-comparison-grid"><div class="s-comparison-side s-comparison-left"><div class="s-comparison-header"><svg class="s-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 6-6 6 6 6m8-12 6 6-6 6M14 3l-4 18"/></svg><span class="s-comparison-filename">middleware.ts</span><span class="s-comparison-tag">before</span></div><div class="s-comparison-body"><pre class="s-comparison-pre"><code>import { NextRequest } from 'next/server'; export const middleware = async (req: NextRequest) => { let user = undefined; let team = undefined; const token = req.headers.get('token'); if(req.nextUrl.pathname.startsWith('/auth')) { user = await getUserByToken(token); if(!user) { return NextResponse.redirect('/login'); } } if(req.nextUrl.pathname.startsWith('/team')) { user = await getUserByToken(token); if(!user) { return NextResponse.redirect('/login'); } const slug = req.nextUrl.query.slug; team = await getTeamBySlug(slug); if(!team) { return NextResponse.redirect('/'); } } return NextResponse.next();} export const config = { matcher: ['/((?!_next/|_static|_vercel|[\w-]+\.\w+).*)'],};</code></pre></div></div><div class="s-comparison-side s-comparison-right"><div class="s-comparison-header"><svg class="s-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 6-6 6 6 6m8-12 6 6-6 6M14 3l-4 18"/></svg><span class="s-comparison-filename">middleware.ts</span><span class="s-comparison-tag s-tag-after">after</span></div><div class="s-comparison-body"><pre class="s-comparison-pre"><code>import { createMiddleware } from '@app/(auth)/auth/_middleware';import { auth } from '@/app/(auth)/auth/_middleware';import { team } from '@/app/(team)/team/_middleware'; const middlewares = { '/auth{/:path?}': auth, '/team{/:slug?}': [ auth, team ],}; export const middleware = createMiddleware(middlewares); export const config = { matcher: ['/((?!_next/|_static|_vercel|[\w-]+\.\w+).*)'],};</code></pre></div></div></div><div class="s-comparison-badge" aria-hidden="true">VS</div></div> </div> Progress indicator pinned at top of viewport tracking scroll depth.
import { ScrollProgress } from "@/components/sarahui/scroll-progress" export default function ScrollProgressDemo() { return ( <div className="z-10 rounded-lg p-4"> <ScrollProgress className="top-[65px]" /> <h2 className="pb-4 font-bold"> Note: The scroll progress is shown below the navbar of the page. </h2> </div> )}
Add ScrollProgress to your project.
$ npx sarahui add scroll-progress
import { ScrollProgress } from 'sarahui';
<ScrollProgress />
import { ScrollProgress } from "@/components/sarahui/scroll-progress"; export function ScrollProgressDemo() { return <ScrollProgress position="top" height={3} />;}
import { ScrollProgress } from "@/components/sarahui/scroll-progress"; export function ScrollProgressBottom() { return <ScrollProgress position="bottom" height={4} />;}
Configuration parameters and properties for ScrollProgress.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | - |
The class name to be applied to the component |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.ScrollProgress({ color: "#5B3DF5" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;width:100%;height:300px;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);overflow:hidden;display:flex;flex-direction:column;"> <div class="s-scroll-progress" style="height:3px;--scroll-color:#5B3DF5;" aria-hidden="true"><div class="s-scroll-progress-bar" data-s-scroll-progress></div></div> <div data-scroll-content style="flex:1;overflow-y:auto;padding:20px;"> <h4 style="margin:0 0 6px;">Scroll Progress Indicator</h4> <p class="s-small s-muted" style="margin-bottom:14px;">Scroll inside this panel to see the reactive top progress bar track your scroll depth.</p> <div style="padding:12px 14px;border-radius:8px;background:var(--s-bg-subtle);margin-bottom:8px;border:1px solid var(--s-border-default);"> <strong style="font-size:13px;display:block;">Section 1: Design Token Parity</strong> <p class="s-small s-muted" style="margin:4px 0 0;">Engineered with native browser APIs and zero external runtime dependencies.</p> </div> <div style="padding:12px 14px;border-radius:8px;background:var(--s-bg-subtle);margin-bottom:8px;border:1px solid var(--s-border-default);"> <strong style="font-size:13px;display:block;">Section 2: Design Token Parity</strong> <p class="s-small s-muted" style="margin:4px 0 0;">Engineered with native browser APIs and zero external runtime dependencies.</p> </div> <div style="padding:12px 14px;border-radius:8px;background:var(--s-bg-subtle);margin-bottom:8px;border:1px solid var(--s-border-default);"> <strong style="font-size:13px;display:block;">Section 3: Design Token Parity</strong> <p class="s-small s-muted" style="margin:4px 0 0;">Engineered with native browser APIs and zero external runtime dependencies.</p> </div> <div style="padding:12px 14px;border-radius:8px;background:var(--s-bg-subtle);margin-bottom:8px;border:1px solid var(--s-border-default);"> <strong style="font-size:13px;display:block;">Section 4: Design Token Parity</strong> <p class="s-small s-muted" style="margin:4px 0 0;">Engineered with native browser APIs and zero external runtime dependencies.</p> </div> <div style="padding:12px 14px;border-radius:8px;background:var(--s-bg-subtle);margin-bottom:8px;border:1px solid var(--s-border-default);"> <strong style="font-size:13px;display:block;">Section 5: Design Token Parity</strong> <p class="s-small s-muted" style="margin:4px 0 0;">Engineered with native browser APIs and zero external runtime dependencies.</p> </div> <div style="padding:12px 14px;border-radius:8px;background:var(--s-bg-subtle);margin-bottom:8px;border:1px solid var(--s-border-default);"> <strong style="font-size:13px;display:block;">Section 6: Design Token Parity</strong> <p class="s-small s-muted" style="margin:4px 0 0;">Engineered with native browser APIs and zero external runtime dependencies.</p> </div> </div> </div> High-intensity chromatic neon glowing border card.
import { NeonGradientCard } from "@/components/sarahui/neon-gradient-card" export default function NeonGradientCardDemo() { return ( <NeonGradientCard className="max-w-sm items-center justify-center text-center"> <span className="pointer-events-none z-10 h-full bg-linear-to-br from-[#ff2975] from-35% to-[#00FFF1] bg-clip-text text-center text-3xl leading-none font-bold tracking-tighter text-balance whitespace-pre-wrap text-transparent md:text-5xl xl:text-6xl dark:drop-shadow-[0_5px_5px_rgba(0,0,0,0.8)]"> Neon Gradient Card </span> </NeonGradientCard> )}
Add NeonGradientCard to your project.
$ npx sarahui add neon-gradient-card
import { NeonGradientCard } from 'sarahui';
<NeonGradientCard> <div className="p-4"> <p>Hello</p> <span>Hover me</span> </div></NeonGradientCard>
Access 120 components with zero dependencies.
import { NeonGradientCard } from "@/components/sarahui/neon-gradient-card"; export function NeonGradientCardDemo() { return ( <NeonGradientCard neonColors={{ firstColor: "#5B3DF5", secondColor: "#FF2E93" }} > <h3>Pro Workspace</h3> </NeonGradientCard> );}
Optimized for ultra-low latency CDN deployments.
import { NeonGradientCard } from "@/components/sarahui/neon-gradient-card"; export function NeonGradientCardEmerald() { return ( <NeonGradientCard neonColors={{ firstColor: "#10B981", secondColor: "#00E5FF" }} borderSize={1.5} > <h3>Production Edge</h3> </NeonGradientCard> );}
Configuration parameters and properties for NeonGradientCard.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | - |
The class name to be applied to the component |
children |
ReactNode | - |
Children elements |
borderSize |
number | 5 |
The size of the border |
borderRadius |
number | 20 |
The size of the radius |
neonColors |
object | { firstColor: "#ff00aa", secondColor: "#00FFF1" } |
The colors of the neon gradient |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.NeonGradientCard({ children: "Neon Card with Glowing Borders" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:300px;"> <div style="max-width:380px;width:100%;"> <div class="s-neon-card" style="--neon-first:#178BFF;--neon-second:#A020FF;--neon-radius:16px;--neon-border:2px;"><div class="s-neon-card-glow" aria-hidden="true"></div><div class="s-neon-card-border" aria-hidden="true"></div><div class="s-neon-card-inner"> <div style="padding:28px 20px;text-align:center;"> <span style="display:block;font-size:clamp(2rem, 5vw, 3rem);font-weight:800;line-height:1.1;letter-spacing:-0.03em;background:linear-gradient(135deg, #ff2975 35%, #00FFF1 100%);-webkit-background-clip:text;-webkit-text-fill-color:transparent;"> Neon Gradient Card </span> </div> </div></div> </div> </div> Retro pop-art comic book typography with halftone dot shading and bold outlines.
import { ComicText } from "@/components/sarahui/comic-text" export default function ComicTextDemo() { return ( <div className="space-y-8 text-center"> <ComicText fontSize={5}>BOOM!</ComicText> </div> )}
Add ComicText to your project.
$ npx sarahui add comic-text
import { ComicText } from 'sarahui';
<ComicText>Comic Text</ComicText>
import { ComicText } from "@/components/sarahui/comic-text"; export function ComicTextDemo() { return <ComicText text="BOOM!" color="#FF3366" />;}
import { ComicText } from "@/components/sarahui/comic-text"; export function ComicTextAction() { return <ComicText text="SUPERCHARGED!" color="#FFB800" />;}
Configuration parameters and properties for ComicText.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | - |
The class name to be applied to the component |
fontSize |
number | 4 |
The font size of the text |
style |
CSSProperties | #ffffff |
The style of the text |
children |
string | - |
The text to be displayed |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.ComicText({ text: "POW! SARAHUI" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="padding:24px;text-align:center;"><div class="s-comic-text" style="--comic-font-size:3.5rem;--comic-dot-color:#EF4444;--comic-bg-color:#FACC15;">POW! BOOM! SARAHUI</div></div>Dynamic kinetic typography flowing smoothly with momentum-reactive velocity.
import { KineticText } from "@/components/sarahui/kinetic-text" export default function KineticTextDemo() { return ( <div className="relative justify-center"> <KineticText text="Nostalgia" className="text-[6rem] tracking-[-5%] [font-optical-sizing:auto]" /> </div> )}
Add KineticText to your project.
$ npx sarahui add kinetic-text
import { KineticText } from 'sarahui';
<KineticText text="some text" />
import { KineticText } from "@/components/sarahui/kinetic-text"; export function KineticTextDemo() { return <KineticText text="VELOCITY TYPOGRAPHY • ACCELERATED DESIGN • " />;}
import { KineticText } from "@/components/sarahui/kinetic-text"; export function KineticTextFast() { return <KineticText text="HIGH SPEED DESIGN • MAXIMUM QUALITY • " speed={1.8} />;}
Configuration parameters and properties for KineticText.
| Prop | Type | Default | Description |
|---|---|---|---|
text |
string | - |
The text content to render with the kinetic effect. |
as |
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | "span" | "h1" |
The HTML element to render as. |
className |
string | - |
Additional class names for the wrapper element. |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.KineticText({ text: "KINETIC MOTION" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:220px;background:var(--s-bg-surface);border-radius:12px;border:1px solid var(--s-border-default);overflow:hidden;"><h2 class="s-kinetic-text"><span class="s-kinetic-char" aria-hidden="true">K</span><span class="s-kinetic-char" aria-hidden="true">I</span><span class="s-kinetic-char" aria-hidden="true">N</span><span class="s-kinetic-char" aria-hidden="true">E</span><span class="s-kinetic-char" aria-hidden="true">T</span><span class="s-kinetic-char" aria-hidden="true">I</span><span class="s-kinetic-char" aria-hidden="true">C</span><span class="s-sr-only">KINETIC</span></h2></div>Celebratory multi-particle burst explosion on click.
import { Button } from "@/components/sarahui/button"import { CoolMode } from "@/components/sarahui/cool-mode" export default function CoolModeDemo() { return ( <div className="relative justify-center"> <CoolMode> <Button>Click Me!</Button> </CoolMode> </div> )}
Add CoolMode to your project.
$ npx sarahui add cool-mode
import { CoolMode } from 'sarahui';
<CoolMode> <button>Click me</button></CoolMode>
import { CoolMode } from "@/components/sarahui/cool-mode"; export function CoolModeDemo() { return ( <CoolMode> <Button>Celebrate Release 🎉</Button> </CoolMode> );}
import { CoolMode } from "@/components/sarahui/cool-mode"; export function CoolModeCustom() { return ( <CoolMode particle="stars"> <Button variant="outline">Super Star ⭐</Button> </CoolMode> );}
Configuration parameters and properties for CoolMode.
| Prop | Type | Default | Description |
|---|---|---|---|
particle |
String | "circle" |
The particle URL for a custom particle |
size |
Number | Varies |
Size of the particle |
particleCount |
Number | Varies |
The number of particles to generate |
speedHorz |
Number | Varies |
Horizontal speed of the particles |
speedUp |
Number | Varies |
Upward speed of the particles |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.CoolMode({ children: UI.Button({ label: "Click for Particles" }) }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="demo-wrap" style="align-items:center;justify-content:center;padding:32px 16px;"><div class="s-cool-mode" data-s-cool-mode="spark"><button type="button" class="s-button s-button--primary s-button--md "><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 2 2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5L2 12l7.5-2.5Z"/></svg><span>Click for Particle Burst</span></button></div></div>Retro pixel distortion canvas filter with interactive resolution calibration.
import { PixelImage } from "@/components/sarahui/pixel-image" export default function Home() { return ( <PixelImage src="/pixel-image-demo.jpg" customGrid={{ rows: 4, cols: 6 }} grayscaleAnimation /> )}
Add PixelImage to your project.
$ npx sarahui add pixel-image
import { PixelImage } from 'sarahui';
<PixelImage src="/pixel-image-demo.jpg" grid="8x8" />
import { PixelImage } from "@/components/sarahui/pixel-image"; export function PixelImageDemo() { return <PixelImage pixelSize={8} width={280} height={180} />;}
import { PixelImage } from "@/components/sarahui/pixel-image"; export function PixelImageFine() { return <PixelImage pixelSize={4} width={280} height={180} />;}
Configuration parameters and properties for PixelImage.
| Prop | Type | Default | Description |
|---|---|---|---|
src |
string | — |
The image source URL |
grid |
"6x4" | "8x8" | "8x3" | "4x6" | "3x8" | "8x8" |
Predefined grid layout |
customGrid |
{ rows: number; cols: number; } | — |
Custom grid layout (overrides `grid`) |
grayscaleAnimation |
boolean | true |
Whether to animate from grayscale to color |
pixelFadeInDuration |
number | 1000 |
Duration (ms) for each pixel fade-in animation |
maxAnimationDelay |
number | 1200 |
Maximum random delay (ms) for pixel animation |
colorRevealDelay |
number | 1500 |
Delay (ms) before revealing color |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.PixelImage({ pixelSize: 8, width: 240, height: 160 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;justify-content:center;width:100%;"><div class="s-pixel-image-wrap" role="img" aria-label="Pixel image"><div class="s-pixel-piece" style="clip-path:polygon(0% 0%, 16.666666666666668% 0%, 16.666666666666668% 25%, 0% 25%);animation-delay:88ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(16.666666666666668% 0%, 33.333333333333336% 0%, 33.333333333333336% 25%, 16.666666666666668% 25%);animation-delay:411ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(33.333333333333336% 0%, 50% 0%, 50% 25%, 33.333333333333336% 25%);animation-delay:706ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(50% 0%, 66.66666666666667% 0%, 66.66666666666667% 25%, 50% 25%);animation-delay:462ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(66.66666666666667% 0%, 83.33333333333334% 0%, 83.33333333333334% 25%, 66.66666666666667% 25%);animation-delay:4ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(83.33333333333334% 0%, 100% 0%, 100% 25%, 83.33333333333334% 25%);animation-delay:253ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(0% 25%, 16.666666666666668% 25%, 16.666666666666668% 50%, 0% 50%);animation-delay:92ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(16.666666666666668% 25%, 33.333333333333336% 25%, 33.333333333333336% 50%, 16.666666666666668% 50%);animation-delay:3ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(33.333333333333336% 25%, 50% 25%, 50% 50%, 33.333333333333336% 50%);animation-delay:47ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(50% 25%, 66.66666666666667% 25%, 66.66666666666667% 50%, 50% 50%);animation-delay:349ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(66.66666666666667% 25%, 83.33333333333334% 25%, 83.33333333333334% 50%, 66.66666666666667% 50%);animation-delay:326ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(83.33333333333334% 25%, 100% 25%, 100% 50%, 83.33333333333334% 50%);animation-delay:201ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(0% 50%, 16.666666666666668% 50%, 16.666666666666668% 75%, 0% 75%);animation-delay:789ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(16.666666666666668% 50%, 33.333333333333336% 50%, 33.333333333333336% 75%, 16.666666666666668% 75%);animation-delay:237ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(33.333333333333336% 50%, 50% 50%, 50% 75%, 33.333333333333336% 75%);animation-delay:702ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(50% 50%, 66.66666666666667% 50%, 66.66666666666667% 75%, 50% 75%);animation-delay:350ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(66.66666666666667% 50%, 83.33333333333334% 50%, 83.33333333333334% 75%, 66.66666666666667% 75%);animation-delay:289ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(83.33333333333334% 50%, 100% 50%, 100% 75%, 83.33333333333334% 75%);animation-delay:28ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(0% 75%, 16.666666666666668% 75%, 16.666666666666668% 100%, 0% 100%);animation-delay:682ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(16.666666666666668% 75%, 33.333333333333336% 75%, 33.333333333333336% 100%, 16.666666666666668% 100%);animation-delay:650ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(33.333333333333336% 75%, 50% 75%, 50% 100%, 33.333333333333336% 100%);animation-delay:607ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(50% 75%, 66.66666666666667% 75%, 66.66666666666667% 100%, 50% 100%);animation-delay:729ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(66.66666666666667% 75%, 83.33333333333334% 75%, 83.33333333333334% 100%, 66.66666666666667% 100%);animation-delay:351ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div><div class="s-pixel-piece" style="clip-path:polygon(83.33333333333334% 75%, 100% 75%, 100% 100%, 83.33333333333334% 100%);animation-delay:564ms;"><img src="assets/sample-1.svg" alt="" class="s-pixel-img s-pixel-gray"></div></div></div>Button with smooth continuous harmonic pulsating glow rings.
import { PulsatingButton } from "@/components/sarahui/pulsating-button" export default function PulsatingButtonDemo() { return <PulsatingButton>Join Affiliate Program</PulsatingButton>}
Add PulsatingButton to your project.
$ npx sarahui add pulsating-button
import { PulsatingButton } from 'sarahui';
<PulsatingButton>Pulsating Button</PulsatingButton>
import { PulsatingButton } from "@/components/sarahui/pulsating-button"; export function PulsatingButtonDemo() { return <PulsatingButton pulseColor="#5B3DF5">Join Live Stream</PulsatingButton>;}
import { PulsatingButton } from "@/components/sarahui/pulsating-button"; export function PulsatingButtonRipple() { return <PulsatingButton variant="ripple" pulseColor="#FF3366">Emergency Alert</PulsatingButton>;}
Configuration parameters and properties for PulsatingButton.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
React.ReactNode | - |
The content of the button. |
className |
string | - |
Additional class names for the button. |
pulseColor |
string | Derived from button background |
Any valid CSS color for the pulsing waves. |
duration |
string | 1.5s |
The duration of one pulse. |
distance |
string | 8px |
How far the pulse expands from the button. |
variant |
"pulse" | "ripple" | pulse |
The pulse animation style. |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.PulsatingButton({ label: "Explore Components" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:220px;"> <button type="button" class="s-pulsating-button" style="--pulse-color:#5B3DF5;"><div class="s-pulse-ring"></div><span>Join Affiliate Program</span></button> </div> Hyperspace 3D perspective warp tunnel background.
import { Card, CardContent, CardDescription, CardTitle,} from "@/components/sarahui/card"import { WarpBackground } from "@/components/sarahui/warp-background" export default function ExampleComponentDemo() { return ( <WarpBackground> <Card className="w-80"> <CardContent className="flex flex-col gap-2 p-4"> <CardTitle>Congratulations on Your Promotion!</CardTitle> <CardDescription> Your hard work and dedication have paid off. We're thrilled to see you take this next step in your career. Keep up the fantastic work! </CardDescription> </CardContent> </Card> </WarpBackground> )}
Add WarpBackground to your project.
$ npx sarahui add warp-background
import { WarpBackground } from 'sarahui';
<WarpBackground> <div className="w-80"> <p>Warp Background</p> <p>This is a component that creates a warp background effect.</p> </div></WarpBackground>
import { WarpBackground } from "@/components/sarahui/warp-background"; export function WarpBackgroundDemo() { return ( <div className="relative h-80 w-full overflow-hidden rounded-xl bg-black"> <WarpBackground perspective={120} /> </div> );}
import { WarpBackground } from "@/components/sarahui/warp-background"; export function WarpBackgroundDeep() { return <WarpBackground perspective={80} gridColor="rgba(0,229,255,0.35)" speed={2} />;}
Configuration parameters and properties for WarpBackground.
| Prop | Type | Default | Description |
|---|---|---|---|
children |
React.ReactNode | - |
The content to be put inside the warp animation |
perspective |
number | 100 |
The perspective of the warp animation |
beamsPerSide |
number | 3 |
The number of beams per side |
beamSize |
number | 5 |
The size of the beams |
beamDelayMax |
number | 3 |
The maximum delay of the beams |
beamDelayMin |
number | 0 |
The minimum delay of the beams |
beamDuration |
number | 3 |
The duration of the beams |
gridColor |
string | "var(--border)" |
The color of the grid lines |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.WarpBackground({ children: "Hyperspace Warp Tunnel" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:380px;width:100%;display:flex;align-items:center;justify-content:center;border-radius:12px;border:1px solid var(--s-border-default);overflow:hidden;background:var(--s-bg-surface);"> <div class="s-warp-container"><div class="s-warp-perspective"><div class="s-warp-beam" style="--warp-i:0;"></div><div class="s-warp-beam" style="--warp-i:1;"></div><div class="s-warp-beam" style="--warp-i:2;"></div><div class="s-warp-beam" style="--warp-i:3;"></div><div class="s-warp-beam" style="--warp-i:4;"></div><div class="s-warp-beam" style="--warp-i:5;"></div></div><div class="s-warp-content"> <div class="s-card" style="width:320px;padding:24px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);border-radius:14px;box-shadow:0 4px 12px rgba(0,0,0,0.06);text-align:left;position:relative;z-index:2;"> <h4 style="margin:0 0 8px;font-size:16px;font-weight:600;color:var(--s-text-primary);">Congratulations on Your Promotion!</h4> <p class="s-small s-muted" style="margin:0;line-height:1.5;">Your hard work and dedication have paid off. We're thrilled to see you take this next step in your career. Keep up the fantastic work!</p> </div> </div></div> </div> Interactive button with expanding circle fill and sliding directional arrow.
import { InteractiveHoverButton } from "@/components/sarahui/interactive-hover-button" export default function InteractiveHoverButtonDemo() { return <InteractiveHoverButton>Hover Me</InteractiveHoverButton>}
Add InteractiveHoverButton to your project.
$ npx sarahui add interactive-hover-button
import { InteractiveHoverButton } from 'sarahui';
<InteractiveHoverButton>Interactive Hover Button</InteractiveHoverButton>
import { InteractiveHoverButton } from "@/components/sarahui/interactive-hover-button"; export function InteractiveHoverButtonDemo() { return <InteractiveHoverButton text="Get Started" />;}
import { InteractiveHoverButton } from "@/components/sarahui/interactive-hover-button"; export function InteractiveHoverButtonExplore() { return <InteractiveHoverButton text="Explore Library" />;}
Configuration parameters and properties for InteractiveHoverButton.
| Prop | Type | Default | Description |
|---|---|---|---|
text |
string | Button |
The text to be displayed inside the button |
className |
string | - |
Additional class names to style the component |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.InteractiveHoverButton({ label: "Get Started" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="display:flex;align-items:center;justify-content:center;padding:48px 16px;min-height:220px;"> <button type="button" class="s-interactive-hover-btn"><span class="s-interactive-hover-text">Hover Me</span><div class="s-interactive-hover-dot"></div><div class="s-interactive-hover-arrow"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"/></svg></div></button> </div> Animated radial gauge progress indicator with gradient stroke and centered value.
"use client" import { useEffect, useState } from "react" import { AnimatedCircularProgressBar } from "@/components/sarahui/animated-circular-progress-bar" export default function AnimatedCircularProgressBarDemo() { const [value, setValue] = useState(0) useEffect(() => { const handleIncrement = (prev: number) => { if (prev === 100) { return 0 } return prev + 10 } setValue(handleIncrement) const interval = setInterval(() => setValue(handleIncrement), 2000) return () => clearInterval(interval) }, []) return ( <AnimatedCircularProgressBar value={value} gaugePrimaryColor="rgb(79 70 229)" gaugeSecondaryColor="rgba(0, 0, 0, 0.1)" /> )}
Add AnimatedCircularProgressBar to your project.
$ npx sarahui add animated-circular-progress-bar
import { AnimatedCircularProgressBar } from 'sarahui';
<AnimatedCircularProgressBar />
import { AnimatedCircularProgressBar } from "@/components/sarahui/animated-circular-progress-bar"; export function AnimatedCircularProgressBarDemo() { return ( <AnimatedCircularProgressBar max={100} min={0} value={72} gaugePrimaryColor="#5B3DF5" gaugeSecondaryColor="rgba(91,61,245,0.15)" /> );}
import { AnimatedCircularProgressBar } from "@/components/sarahui/animated-circular-progress-bar"; export function AnimatedCircularProgressBarTarget() { return ( <AnimatedCircularProgressBar max={100} min={0} value={98} gaugePrimaryColor="#10B981" gaugeSecondaryColor="rgba(16,185,129,0.15)" /> );}
Configuration parameters and properties for AnimatedCircularProgressBar.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | - |
The class name to be applied to the component |
max |
number | 100 |
The maximum value of the gauge |
min |
number | 0 |
The minimum value of the gauge |
value |
number | 0 |
The current value of the gauge |
gaugePrimaryColor |
string | - |
The primary color of the gauge |
gaugeSecondaryColor |
string | - |
The secondary color of the gauge |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.AnimatedCircularProgressBar({ value: 75 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="demo-wrap" style="align-items:center;gap:32px;"><div class="s-circular-progress-wrap" style="--circle-val:75;"><svg class="s-circular-progress-svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="45" class="s-circular-progress-bg" stroke="rgba(255,255,255,0.1)" stroke-width="8" fill="none"/><circle cx="50" cy="50" r="45" class="s-circular-progress-bar" stroke="#5B3DF5" stroke-width="8" stroke-dasharray="282.74" stroke-dashoffset="70.69" stroke-linecap="round" fill="none"/></svg><span class="s-circular-progress-text">75%</span></div><div class="s-circular-progress-wrap" style="--circle-val:100;"><svg class="s-circular-progress-svg" viewBox="0 0 100 100"><circle cx="50" cy="50" r="45" class="s-circular-progress-bg" stroke="rgba(255,255,255,0.1)" stroke-width="8" fill="none"/><circle cx="50" cy="50" r="45" class="s-circular-progress-bar" stroke="#10B981" stroke-width="8" stroke-dasharray="282.74" stroke-dashoffset="0.00" stroke-linecap="round" fill="none"/></svg><span class="s-circular-progress-text">100%</span></div></div>Ambient colored light aura radiating softly behind foreground containers.
Soft diffuse radiant backdrop aura.
import { Backlight } from "@/components/sarahui/backlight"; export default function BacklightDemo() { return ( <div className="relative flex items-center justify-center p-8"> <Backlight blur={24}> <div className="relative flex flex-col items-center justify-center rounded-2xl border border-border bg-card p-8 shadow-xl backdrop-blur-md"> <span className="text-xs font-semibold tracking-wider uppercase text-primary">Ambient Aura</span> <h3 className="mt-2 text-2xl font-bold tracking-tight text-foreground">Backlight Glow</h3> <p className="mt-1 text-sm text-muted-foreground">Soft radiant aura projected behind container elements.</p> </div> </Backlight> </div> );}
Add Backlight to your project.
$ npx sarahui add backlight
import { Backlight } from 'sarahui';
<Backlight> <img src="https://some-url/image.png" /></Backlight>
Soft diffuse radiant backdrop aura.
import { Backlight } from "@/components/sarahui/backlight"; export function BacklightImage() { return ( <Backlight color="rgba(91, 61, 245, 0.4)"> <img src="/hero-preview.png" alt="Hero" /> </Backlight> );}
Soft diffuse radiant backdrop aura.
import { Backlight } from "@/components/sarahui/backlight"; export function BacklightSVG() { return ( <Backlight color="rgba(0, 229, 255, 0.5)"> <BrandLogo /> </Backlight> );}
Configuration parameters and properties for Backlight.
| Prop | Type | Default | Description |
|---|---|---|---|
blur |
number | 20 |
The blur intensity of the backlight glow effect. |
children |
React.ReactElement | - |
The video, image, or SVG element to apply the backlight effect to. |
className |
string | - |
Additional class names for the wrapper div. |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Backlight({ color: "rgba(91,61,245,0.4)", children: UI.html`<div style="padding:20px;">Card with Backlight</div>` }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="padding:20px 0;display:flex;justify-content:center;width:100%;"><div class="s-backlight-wrap"><svg class="s-backlight-filter" width="0" height="0" aria-hidden="true"><defs><filter id="s-backlight-30" y="-50%" x="-50%" width="200%" height="200%"><feGaussianBlur in="SourceGraphic" stdDeviation="20" result="blurred"></feGaussianBlur><feColorMatrix type="saturate" in="blurred" values="4"></feColorMatrix><feComposite in="SourceGraphic" operator="over"></feComposite></filter></defs></svg><div class="s-backlight-content" style="filter:url(#s-backlight-30);"><div style="padding:24px 32px;background:var(--s-bg-surface);border:1px solid var(--s-border-default);border-radius:12px;text-align:center;"><strong>Ambient Backlight Glow</strong><p class="s-small s-muted" style="margin:4px 0 0;">Soft diffuse radiant backdrop aura.</p></div></div></div></div>Hacker-style random character cypher stream with glowing lead glyphs.
import { GlyphMatrix } from "@/components/sarahui/glyph-matrix"; export function GlyphMatrixDemo() { const color = "var(--s-brand-indigo, #5B3DF5)"; return ( <div className="border-border bg-background relative h-[400px] w-full overflow-hidden rounded-lg border"> <GlyphMatrix glyphs="01·•+*/\<>=" cellSize={14} mutationRate={0.04} interval={90} fadeBottom={0.6} color={color} /> </div> );}
Add GlyphMatrix to your project.
$ npx sarahui add glyph-matrix
import { GlyphMatrix } from 'sarahui';
<div className="border-border bg-background h-[400px] w-full overflow-hidden rounded-lg border"> <GlyphMatrix /></div>
import { GlyphMatrix } from "@/components/sarahui/glyph-matrix"; export function GlyphMatrixDemo() { return ( <div className="relative h-80 w-full overflow-hidden rounded-xl bg-black"> <GlyphMatrix color="#00FF66" /> </div> );}
import { GlyphMatrix } from "@/components/sarahui/glyph-matrix"; export function GlyphMatrixViolet() { return <GlyphMatrix color="#A855F7" speed={1.5} />;}
Configuration parameters and properties for GlyphMatrix.
| Prop | Type | Default | Description |
|---|---|---|---|
glyphs |
string | "01·•+*/\\<>=" |
Characters to randomly pick from. |
cellSize |
number | 14 |
Cell size in pixels and font size. |
mutationRate |
number | 0.04 |
Probability a cell mutates each tick. |
interval |
number | 90 |
Tick interval in milliseconds. |
className |
string | - |
Classes applied to the canvas element. |
fadeBottom |
number | 0.6 |
Fade strength toward the bottom of the grid. |
color |
string | "#6B7280" |
Glyph color (any CSS color). |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.GlyphMatrix({ color: "#5B3DF5" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="position:relative;height:180px;width:100%;overflow:hidden;border-radius:12px;border:1px solid var(--s-border-default);background:var(--s-bg-surface);display:grid;place-items:center;"><div class="s-glyph-matrix-wrap" aria-hidden="true"><canvas class="s-glyph-matrix-canvas" data-s-glyph-matrix data-glyphs="01·•+*/\<>=" data-cell-size="14" data-mutation-rate="0.04" data-interval="90" data-color="#6B7280"></canvas></div><span style="position:relative;z-index:2;font-weight:700;font-size:14px;letter-spacing:2px;color:var(--s-text-primary);">SARAHUI CYPHER</span></div>Morphing subscription button that smoothly transitions state with checkmark.
import { AnimatedSubscribeButton } from "@/components/sarahui/animated-subscribe-button"; export default function AnimatedSubscribeButtonDemo() { return ( <div className="flex flex-wrap items-center justify-center gap-4 p-6"> <AnimatedSubscribeButton initialText="Subscribe" subscribedText="Subscribed" /> <AnimatedSubscribeButton initialText="Follow updates" subscribedText="Following" isSubscribed={true} attrs={{ "aria-describedby": "subscribe-hint" }} /> <span id="subscribe-hint" className="text-xs text-muted-foreground">Toggle to receive release notes.</span> </div> );}
Add AnimatedSubscribeButton to your project.
$ npx sarahui add animated-subscribe-button
import { AnimatedSubscribeButton } from 'sarahui';
<AnimatedSubscribeButton buttonColor="#000000" buttonTextColor="#ffffff" subscribeStatus={false} initialText="Subscribe" changeText="Subscribed ✓"/>
import { AnimatedSubscribeButton } from "@/components/sarahui/animated-subscribe-button"; export function AnimatedSubscribeButtonDemo() { return ( <AnimatedSubscribeButton initialText="Subscribe" subscribedText="Subscribed" /> );}
import { AnimatedSubscribeButton } from "@/components/sarahui/animated-subscribe-button"; export function AnimatedBookmark() { return ( <AnimatedSubscribeButton initialText="Save to Library" subscribedText="Saved in Library" /> );}
Configuration parameters and properties for AnimatedSubscribeButton.
| Prop | Type | Default | Description |
|---|---|---|---|
initialText |
string | 'Subscribe' |
Label shown before the user subscribes |
subscribedText |
string | 'Subscribed' |
Label shown after the subscribed state is reached |
isSubscribed |
boolean | false |
Initial pressed/subscribed state (reflected via aria-pressed) |
attrs |
Record<string, any> | {} |
Extra HTML attributes forwarded to the button |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.AnimatedSubscribeButton({ initialText: "Subscribe", subscribedText: "Subscribed" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="demo-wrap"><button type="button" class="s-animated-subscribe-btn" data-s-subscribe-btn aria-pressed="false"><span class="s-subscribe-initial"><span class="s-sub-icon"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 17h14l-2-3V9a5 5 0 0 0-10 0v5Zm5 3h4"/></svg></span><span>Subscribe</span></span><span class="s-subscribe-active"><span class="s-sub-icon"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4 12 5 5L20 6"/></svg></span><span>Subscribed</span></span></button></div>Visible labels, helpful context, and explicit error messages.
A good place to stay connected.
Use at least 3 characters.
import { Input } from "@/components/sarahui/input";import { MailIcon } from "sarahui/icons"; export function InputDemo() { return ( <div className="grid w-full max-w-sm gap-4"> <Input id="email" label="Email address" type="email" icon={<MailIcon />} placeholder="hello@yourstudio.com" helper="A good place to stay connected." /> </div> );}
Add Input to your project.
$ npx sarahui add input
import { Input } from 'sarahui';
<Input type="email" placeholder="Email" />
A good place to stay connected.
import { Input } from "@/components/sarahui/input"; export function InputDemo() { return ( <Input label="Email address" type="email" icon="mail" placeholder="hello@yourstudio.com" helper="A good place to stay connected." /> );}
Use at least 3 characters.
import { Input } from "@/components/sarahui/input"; export function InputError() { return ( <Input label="Workspace URL" error="Use at least 3 characters." defaultValue="ab" /> );}
import { Input } from "@/components/sarahui/input"; export function InputDisabled() { return <Input label="Organization ID" defaultValue="ORG-98214-X" disabled />;}
Configuration parameters and properties for Input.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | required |
Unique identifier for label association |
label |
string | required |
Accessible form control label |
type |
string | 'text' |
HTML input type (text, email, password, etc.) |
placeholder |
string | '' |
Placeholder hint |
helper |
string | '' |
Supporting instructional text |
error |
string | '' |
Validation error message |
required |
boolean | false |
Marks field as required |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Input({ id: "email", label: "Email address", type: "email", helper: "We will use this to contact you.", attrs: { name: "email", required: true, placeholder: "you@example.com" } }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-stack"><div class="s-field"><label class="s-label" for="demo-input-email">Email address</label><div class="s-input-wrap"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" aria-hidden="true"><rect width="20" height="16" x="2" y="4" rx="2" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg><input placeholder="hello@yourstudio.com" id="demo-input-email" type="email" class="s-input s-input--icon" aria-describedby="demo-input-email-help"></div><p id="demo-input-email-help" class="s-field-help">A good place to stay connected.</p></div><div class="s-field"><label class="s-label" for="demo-input-error">Workspace URL</label><div class="s-input-wrap"><input value="ab" id="demo-input-error" type="text" class="s-input" aria-invalid="true" aria-describedby="demo-input-error-help"></div><p id="demo-input-error-help" class="s-field-help s-field-error">Use at least 3 characters.</p></div></div>A little more room for detail. Resizable and ready for native forms.
A sentence or two is a great place to start.
import { Textarea } from "@/components/sarahui/textarea"; export function TextareaDemo() { return ( <div className="grid w-full max-w-sm gap-2"> <Textarea id="project-brief" label="Project brief" placeholder="What are you creating?" helper="A sentence or two is a great place to start." rows={4} /> </div> );}
Add Textarea to your project.
$ npx sarahui add textarea
import { Textarea } from 'sarahui';
<Textarea placeholder="Type your message here." />
A sentence or two is a great place to start.
import { Textarea } from "@/components/sarahui/textarea"; export function TextareaDemo() { return ( <Textarea label="Project brief" placeholder="What are you creating?" helper="A sentence or two is a great place to start." /> );}
import { Textarea } from "@/components/sarahui/textarea"; export function TextareaDisabled() { return <Textarea label="System logs" defaultValue="Audit log verified: OK" disabled />;}
Configuration parameters and properties for Textarea.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | required |
Unique element identifier |
label |
string | required |
Form field label |
rows |
number | 4 |
Visible line height rows |
maxlength |
number | 500 |
Maximum character limit |
helper |
string | '' |
Supporting instructions |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Textarea({ id: "brief", label: "Project brief", helper: "A sentence is a good start.", attrs: { name: "brief", rows: 4, maxlength: 500 } }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-field"><label class="s-label" for="demo-textarea">Project brief</label><textarea rows="3" placeholder="What are you creating?" maxlength="500" id="demo-textarea" class="s-input s-textarea" aria-describedby="demo-textarea-help"></textarea><p id="demo-textarea-help" class="s-field-help">A sentence or two is a great place to start.</p></div>Native keyboard, touch, and platform accessibility behavior.
Choose where your project belongs.
import { Select } from "@/components/sarahui/select"; export function SelectDemo() { return ( <div className="grid w-full max-w-sm gap-2"> <Select id="workspace" label="Workspace" options={["Sarah’s studio", "Personal", "Design team"]} helper="Choose where your project belongs." /> </div> );}
Add Select to your project.
$ npx sarahui add select
import { Select } from 'sarahui';
<Select options={["Option 1", "Option 2"]} placeholder="Select an option" />
Choose where your project belongs.
import { Select } from "@/components/sarahui/select"; export function SelectDemo() { return ( <Select label="Workspace" options={["Sarah's studio", "Personal", "Design team"]} /> );}
import { Select } from "@/components/sarahui/select"; export function SelectDisabled() { return <Select label="Region" options={["us-east-1 (Locked)"]} disabled />;}
Configuration parameters and properties for Select.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | required |
Unique element identifier |
label |
string | required |
Visible accessible label |
options |
Array<{ label: string, value: string }> | [] |
Selectable items |
value |
string | '' |
Initial selected value |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Select({ id: "workspace", label: "Workspace", options: [{ label: "Personal", value: "personal" }, { label: "Team", value: "team" }], value: "team", attrs: { name: "workspace" } }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-field"><label class="s-label" for="demo-select">Workspace</label><div class="s-select-wrap"><select id="demo-select" class="s-input s-select" aria-describedby="demo-select-help"><option value="Sarah’s studio">Sarah’s studio</option><option value="Personal">Personal</option><option value="Design team">Design team</option></select><svg class="s-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 9 7 7 7-7"/></svg></div><p id="demo-select-help" class="s-field-help">Choose where your project belongs.</p></div>Independent choices with full labels and mixed selection support.
import { Checkbox } from "@/components/sarahui/checkbox"; export function CheckboxDemo() { return ( <div className="flex flex-col gap-3"> <Checkbox id="c1" label="Product updates" description="Inspiration delivered occasionally." defaultChecked /> <Checkbox id="c2" label="Community announcements" description="News from our community." /> </div> );}
Add Checkbox to your project.
$ npx sarahui add checkbox
import { Checkbox } from 'sarahui';
<Checkbox id="terms" label="Accept terms and conditions" />
import { Checkbox } from "@/components/sarahui/checkbox"; export function CheckboxDemo() { return ( <div className="space-y-2"> <Checkbox label="Product updates" defaultChecked /> <Checkbox label="Community newsletter" /> <Checkbox label="Organization security" disabled defaultChecked /> </div> );}
import { Checkbox } from "@/components/sarahui/checkbox"; export function CheckboxError() { return <Checkbox label="Accept terms" required error="Required field" />;}
Configuration parameters and properties for Checkbox.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | required |
Unique identifier |
label |
string | required |
Visible checkbox label |
description |
string | '' |
Secondary descriptive text |
checked |
boolean | false |
Checked state |
disabled |
boolean | false |
Disables checkbox |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Checkbox({ id: "updates", label: "Product updates", description: "Occasional news from the team.", checked: true, attrs: { name: "updates" } }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-stack"><label class="s-choice s-choice--checkbox" for="demo-checkbox-a"><input id="demo-checkbox-a" type="checkbox" class="s-choice-input s-checkbox" checked aria-describedby="demo-checkbox-a-description"><span class="s-choice-copy"><span>Product updates</span><small id="demo-checkbox-a-description">A little inspiration, once in a while.</small></span></label><label class="s-choice s-choice--checkbox" for="demo-checkbox-b"><input id="demo-checkbox-b" type="checkbox" class="s-choice-input s-checkbox" aria-describedby="demo-checkbox-b-description"><span class="s-choice-copy"><span>Community announcements</span><small id="demo-checkbox-b-description">News and insights from our team.</small></span></label><label class="s-choice s-choice--checkbox" for="demo-checkbox-c"><input id="demo-checkbox-c" type="checkbox" class="s-choice-input s-checkbox" checked disabled aria-describedby="demo-checkbox-c-description"><span class="s-choice-copy"><span>Workspace policy</span><small id="demo-checkbox-c-description">Enforced by organization security.</small></span></label></div>One choice from a related group, with native arrow key navigation.
import { RadioGroup, Radio } from "@/components/sarahui/radio"; export function RadioDemo() { return ( <RadioGroup label="Billing cycle" defaultValue="monthly"> <Radio id="r1" value="monthly" label="Monthly billing" description="Flexible month to month." /> <Radio id="r2" value="yearly" label="Yearly billing" description="Save 20% with annual payments." /> </RadioGroup> );}
Add Radio to your project.
$ npx sarahui add radio
import { Radio } from 'sarahui';
<Radio name="plan" value="pro" label="Pro Plan" />
import { RadioGroup, Radio } from "@/components/sarahui/radio"; export function RadioDemo() { return ( <RadioGroup name="billing" defaultValue="monthly"> <Radio value="monthly" label="Monthly billing" /> <Radio value="yearly" label="Yearly billing (Save 20%)" /> </RadioGroup> );}
import { RadioGroup, Radio } from "@/components/sarahui/radio"; export function RadioDisabled() { return ( <RadioGroup name="plan" defaultValue="std"> <Radio value="std" label="Standard" /> <Radio value="ent" label="Enterprise (Waitlist)" disabled /> </RadioGroup> );}
Configuration parameters and properties for Radio.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | required |
Unique element identifier |
label |
string | required |
Option label |
description |
string | '' |
Option supporting description |
checked |
boolean | false |
Selected state |
disabled |
boolean | false |
Disables option |
Keep one name for related choices and a distinct value for each option.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.html`<fieldset><legend>Billing cycle</legend>${UI.Radio({ id: "monthly", label: "Monthly", checked: true, attrs: { name: "billing", value: "monthly" } })}${UI.Radio({ id: "yearly", label: "Yearly", attrs: { name: "billing", value: "yearly" } })}</fieldset>`; document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
Keep one name for related choices and a distinct value for each option.
<fieldset class="demo-fieldset"><legend>Billing cycle</legend><label class="s-choice s-choice--radio" for="demo-radio-a"><input name="demo-radio" value="monthly" id="demo-radio-a" type="radio" class="s-choice-input s-radio" checked aria-describedby="demo-radio-a-description"><span class="s-choice-copy"><span>Monthly billing</span><small id="demo-radio-a-description">Flexible, month to month payment.</small></span></label><label class="s-choice s-choice--radio" for="demo-radio-b"><input name="demo-radio" value="yearly" id="demo-radio-b" type="radio" class="s-choice-input s-radio" aria-describedby="demo-radio-b-description"><span class="s-choice-copy"><span>Yearly billing</span><small id="demo-radio-b-description">One simple payment for the full year (Save 20%).</small></span></label></fieldset>An immediate on or off control, with native checkbox behavior.
import { Switch } from "@/components/sarahui/switch"; export function SwitchDemo() { return ( <div className="flex flex-col gap-3"> <Switch id="s1" label="Email notifications" description="Get notified about team activity." defaultChecked /> <Switch id="s2" label="Weekly digest" description="Summary sent every Monday." /> </div> );}
Add Switch to your project.
$ npx sarahui add switch
import { Switch } from 'sarahui';
<Switch id="airplane-mode" label="Airplane Mode" />
import { Switch } from "@/components/sarahui/switch"; export function SwitchChecked() { return <Switch label="Real-time sync" defaultChecked />;}
import { Switch } from "@/components/sarahui/switch"; export function SwitchUnchecked() { return <Switch label="Weekly digest" defaultChecked={false} />;}
Configuration parameters and properties for Switch.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | required |
Unique element identifier |
label |
string | required |
Toggle label |
checked |
boolean | false |
On / off switch state |
disabled |
boolean | false |
Disables switch toggle |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Switch({ id: "notifications", label: "Email notifications", checked: true, attrs: { name: "notifications" } }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-stack"><label class="s-choice s-choice--switch" for="demo-switch-a"><input id="demo-switch-a" type="checkbox" class="s-choice-input s-switch" role="switch" checked aria-describedby="demo-switch-a-description"><span class="s-choice-copy"><span>Email notifications</span><small id="demo-switch-a-description">Know when something needs your attention.</small></span></label><label class="s-choice s-choice--switch" for="demo-switch-b"><input id="demo-switch-b" type="checkbox" class="s-choice-input s-switch" role="switch" aria-describedby="demo-switch-b-description"><span class="s-choice-copy"><span>Weekly digest</span><small id="demo-switch-b-description">A concise recap sent every Monday morning.</small></span></label></div>A continuous or discrete range control for numeric values.
import { Slider } from "@/components/sarahui/slider"; export function SliderDemo() { return ( <div className="w-full max-w-sm"> <Slider id="density" label="Interface density" min={0} max={100} defaultValue={64} /> </div> );}
Add Slider to your project.
$ npx sarahui add slider
import { Slider } from 'sarahui';
<Slider defaultValue={[50]} max={100} step={1} />
import { Slider } from "@/components/sarahui/slider"; export function SliderDemo() { return <Slider defaultValue={[75]} max={100} step={1} />;}
import { Slider } from "@/components/sarahui/slider"; export function SliderStepped() { return <Slider defaultValue={[3]} min={1} max={5} step={1} />;}
Configuration parameters and properties for Slider.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | required |
Unique element identifier |
label |
string | required |
Slider control label |
min |
number | 0 |
Minimum value boundary |
max |
number | 100 |
Maximum value boundary |
value |
number | 50 |
Current numeric value |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Slider({ id: "volume", label: "Volume", min: 0, max: 100, value: 60, step: 5 }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-field s-slider-field"><div class="s-between"><label class="s-label" for="demo-slider">Interface density</label><output for="demo-slider">64</output></div><input id="demo-slider" type="range" class="s-slider" value="64" min="0" max="100" step="1" data-s-slider></div>Connected views, keyboard navigation, and consistent focus.
Your workspace brings clarity to the everyday.
Updates appear here as your team works.
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/sarahui/tabs"; export function TabsDemo() { return ( <Tabs defaultValue="overview"> <TabsList> <TabsTrigger value="overview">Overview</TabsTrigger> <TabsTrigger value="activity">Activity</TabsTrigger> <TabsTrigger value="settings">Settings</TabsTrigger> </TabsList> <TabsContent value="overview">Everything in its place.</TabsContent> <TabsContent value="activity">You're all caught up.</TabsContent> <TabsContent value="settings">Manage your preferences.</TabsContent> </Tabs> );}
Add Tabs to your project.
$ npx sarahui add tabs
import { Tabs } from 'sarahui';
<Tabs items={[ { label: "Account", content: "Make changes to your account here." }, { label: "Password", content: "Change your password here." }]} />
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/sarahui/tabs"; export function TabsDemo() { return ( <Tabs defaultValue="account"> <TabsList> <TabsTrigger value="account">Account</TabsTrigger> <TabsTrigger value="password">Password</TabsTrigger> </TabsList> <TabsContent value="account">Account content</TabsContent> </Tabs> );}
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/sarahui/tabs"; export function TabsPill() { return ( <Tabs defaultValue="monthly" variant="pill"> <TabsList> <TabsTrigger value="monthly">Monthly</TabsTrigger> <TabsTrigger value="annual">Annual</TabsTrigger> </TabsList> </Tabs> );}
Configuration parameters and properties for Tabs.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | required |
Tabs group identifier |
items |
Array<{ label: string, content: ReactNode }> | [] |
Tab panels |
active |
number | 0 |
Initial selected tab index |
Arrow keys, Home and End move through tabs; disabled tabs are skipped.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Tabs({ id: "workspace-tabs", label: "Workspace", items: [{ label: "Overview", content: "Your workspace overview." }, { label: "Activity", content: "Recent workspace activity." }, { label: "Settings", content: "Your preferences." }] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
Arrow keys, Home and End move through tabs; disabled tabs are skipped.
<div class="s-tabs s-tabs--line" data-s-tabs><div class="s-tab-list" role="tablist" aria-label="Workspace preview"><button type="button" class="s-tab" id="demo-tabs-tab-0" role="tab" aria-selected="true" aria-controls="demo-tabs-panel-0" tabindex="0">Overview</button><button type="button" class="s-tab" id="demo-tabs-tab-1" role="tab" aria-selected="false" aria-controls="demo-tabs-panel-1" tabindex="-1">Activity</button><button type="button" class="s-tab" id="demo-tabs-tab-2" role="tab" aria-selected="false" aria-controls="demo-tabs-panel-2" tabindex="-1">Settings</button></div><div class="s-tab-panel" id="demo-tabs-panel-0" role="tabpanel" aria-labelledby="demo-tabs-tab-0" tabindex="0"><div class="tab-example"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 3 10 5-10 5L2 8Zm-9 9 9 5 9-5m-18 5 9 5 9-5"/></svg><div><strong>Everything, in its place.</strong><p>Your workspace brings clarity to the everyday.</p></div></div></div><div class="s-tab-panel" id="demo-tabs-panel-1" role="tabpanel" aria-labelledby="demo-tabs-tab-1" tabindex="0" hidden><div class="tab-example"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM12 7v6l4 2"/></svg><div><strong>You’re all caught up.</strong><p>Updates appear here as your team works.</p></div></div></div><div class="s-tab-panel" id="demo-tabs-panel-2" role="tabpanel" aria-labelledby="demo-tabs-tab-2" tabindex="0" hidden><label class="s-choice s-choice--switch" for="demo-tabs-settings"><input id="demo-tabs-settings" type="checkbox" class="s-choice-input s-switch" role="switch" checked><span class="s-choice-copy"><span>Workspace notifications</span></span></label></div></div>A clear path back, with an explicitly announced current page.
import { Breadcrumb, BreadcrumbItem, BreadcrumbLink } from "@/components/sarahui/breadcrumb"; export function BreadcrumbDemo() { return ( <Breadcrumb> <BreadcrumbItem><BreadcrumbLink href="#">Workspace</BreadcrumbLink></BreadcrumbItem> <BreadcrumbItem><BreadcrumbLink href="#">Projects</BreadcrumbLink></BreadcrumbItem> <BreadcrumbItem isCurrent>Design system</BreadcrumbItem> </Breadcrumb> );}
Add Breadcrumb to your project.
$ npx sarahui add breadcrumb
import { Breadcrumb } from 'sarahui';
<Breadcrumb items={[ { label: "Home", href: "/" }, { label: "Components", href: "/components" }, { label: "Breadcrumb" }]} />
import { Breadcrumb, BreadcrumbItem } from "@/components/sarahui/breadcrumb"; export function BreadcrumbDemo() { return ( <Breadcrumb separator="/"> <BreadcrumbItem href="/workspace">Workspace</BreadcrumbItem> <BreadcrumbItem href="/projects">Projects</BreadcrumbItem> <BreadcrumbItem active>Design System</BreadcrumbItem> </Breadcrumb> );}
import { Breadcrumb } from "@/components/sarahui/breadcrumb"; export function BreadcrumbChevron() { return <Breadcrumb separator="chevron" items={items} />;}
Configuration parameters and properties for Breadcrumb.
| Prop | Type | Default | Description |
|---|---|---|---|
items |
Array<{ label: string, href?: string }> | [] |
Navigation hierarchy segments |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Breadcrumb({ items: [{ label: "Workspace", href: "/workspace" }, { label: "Projects", href: "/projects" }, { label: "Brand refresh" }] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<nav class="s-breadcrumb" aria-label="Breadcrumb"><ol><li><a href="#patterns">Workspace</a></li><li><svg class="s-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 4 8 8-8 8"/></svg><a href="#patterns">Projects</a></li><li><svg class="s-icon" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 4 8 8-8 8"/></svg><span aria-current="page">Design system</span></li></ol></nav>Previous, next, and current page. Connect events to your data.
Page 2 of 5
import { Pagination } from "@/components/sarahui/pagination"; export function PaginationDemo() { return <Pagination pages={5} current={2} label="Pagination demo" />;}
Add Pagination to your project.
$ npx sarahui add pagination
import { Pagination } from 'sarahui';
<Pagination pages={10} current={1} />
Page 2 of 5
import { Pagination } from "@/components/sarahui/pagination"; export function PaginationDemo() { return <Pagination totalPages={5} currentPage={2} />;}
import { Pagination } from "@/components/sarahui/pagination"; export function PaginationCompact() { return <Pagination variant="compact" totalPages={10} currentPage={2} />;}
Configuration parameters and properties for Pagination.
| Prop | Type | Default | Description |
|---|---|---|---|
pages |
number | required |
Total number of pages |
current |
number | 1 |
Active page index |
Listen for sarah:pagechange and load or render the requested records.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Pagination({ pages: 8, current: 1, label: "Project pages" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);document.addEventListener('sarah:pagechange', event => { console.log('Render page', event.detail.page);});// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
Listen for sarah:pagechange and load or render the requested records.
<div class="s-stack"><nav class="s-pagination" data-s-pagination data-pages="5" data-current="2" aria-label="Component pagination demo"><button type="button" data-page="prev" data-direction="back" class="s-button s-icon-button s-button--outline s-button--sm" aria-label="Previous page"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 4 8 8-8 8"/></svg></button><button type="button" class="s-page" data-page="1" aria-label="Page 1">1</button><button type="button" class="s-page" data-page="2" aria-label="Page 2" aria-current="page">2</button><button type="button" class="s-page" data-page="3" aria-label="Page 3">3</button><button type="button" class="s-page" data-page="4" aria-label="Page 4">4</button><button type="button" class="s-page" data-page="5" aria-label="Page 5">5</button><button type="button" data-page="next" class="s-button s-icon-button s-button--outline s-button--sm" aria-label="Next page"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 4 8 8-8 8"/></svg></button></nav><p data-page-feedback class="s-small s-muted" aria-live="polite">Page 2 of 5</p></div>Consistent navigation, optional counts, and active states.
A list of actions, keyboard navigation, and menu items.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Menu({ id: "project-menu", label: "Project actions", items: [{ label: "View project", icon: "folder", value: "view" }, { label: "Archive project", icon: "trash", value: "archive", danger: true }] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);document.addEventListener('sarah:menuaction', event => { console.log('Selected action', event.detail.value);});// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
Listen for sarah:menuaction and use event.detail.value to perform your action.
<div class="s-menu" data-s-menu><button type="button" data-s-menu-trigger aria-haspopup="menu" aria-expanded="false" aria-controls="demo-menu" class="s-button s-button--outline s-button--md "><span>Project actions</span><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 9 7 7 7-7"/></svg></button><div class="s-menu-panel" role="menu" id="demo-menu" aria-label="Project actions" hidden><button type="button" role="menuitem" class="s-menu-item" tabindex="-1" data-value="view"><svg class="s-icon" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h7l2 3h9v12H3Zm0 0V3h7l2 3h9v3"/></svg>View workspace</button><button type="button" role="menuitem" class="s-menu-item" tabindex="-1" data-value="copy"><svg class="s-icon" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 15 6-6M7 13l-2 2a3.5 3.5 0 0 0 5 5l3-3m-2-10 2-2a3.5 3.5 0 0 1 5 5l-3 3"/></svg>Copy page link</button><button type="button" role="menuitem" class="s-menu-item" tabindex="-1" data-value="export" disabled><svg class="s-icon" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12m-5-5 5 5 5-5M4 16v5h16v-5"/></svg>Export unavailable</button><button type="button" role="menuitem" class="s-menu-item s-menu-danger" tabindex="-1" data-value="archive"><svg class="s-icon" width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7m4-7v7"/></svg>Archive preview</button></div></div>Search, filter, and navigate without leaving the keyboard.
import { CommandPalette } from "@/components/sarahui/command-palette";import { Button } from "@/components/sarahui/button";import { SearchIcon } from "sarahui/icons"; export default function CommandPaletteDemo() { return ( <div className="flex flex-col items-center justify-center gap-4 p-8"> <Button variant="outline" className="flex items-center gap-2"> <SearchIcon className="size-4 text-muted-foreground" /> <span>Search documentation...</span> <kbd className="ml-4 rounded border bg-muted px-1.5 py-0.5 text-xs text-muted-foreground">⌘K</kbd> </Button> <CommandPalette id="app-search" title="Search library and tokens" items={[ { label: "Introduction", href: "#overview", icon: "grid", keywords: "start guide docs" }, { label: "Components", href: "#components", icon: "layers", keywords: "ui elements widgets" }, { label: "Design Tokens", href: "#foundations", icon: "palette", keywords: "colors spacing typography" }, { label: "Settings", href: "#settings", icon: "settings", keywords: "preferences configuration" } ]} /> </div> );}
Add CommandPalette to your project.
$ npx sarahui add command-palette
import { CommandPalette } from 'sarahui';
<CommandPalette items={[ { label: "Suggestions", items: [{ label: "Calendar" }, { label: "Search Emoji" }] }]} />
import { CommandPalette } from "@/components/sarahui/command-palette"; export function CommandPaletteDemo() { return ( <CommandPalette shortcut="⌘K" placeholder="Search documentation..." /> );}
import { CommandPalette } from "@/components/sarahui/command-palette"; export function CommandPaletteGrouped() { return <CommandPalette groups={commandGroups} />;}
Configuration parameters and properties for CommandPalette.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | required |
Modal dialog identifier |
title |
string | 'Search' |
Accessible modal heading |
items |
Array<{ label: string, href: string, icon?: string, keywords?: string }> | [] |
Searchable directory links |
The trigger and search dialog belong together. Handle sarah:command for your application router.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.html`${UI.Button({ label: "Search", icon: "search", attrs: { "data-s-dialog-open": "app-search" } })}${UI.CommandPalette({ id: "app-search", title: "Search your workspace", items: [{ label: "Projects", href: "#projects", icon: "folder" }, { label: "Settings", href: "#settings", icon: "settings" }] })}`; document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
The trigger and search dialog belong together. Handle sarah:command for your application router.
<div class="s-row" style="gap:12px;align-items:center;"><button type="button" data-s-dialog-open="library-search" class="s-button s-button--outline s-button--md "><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M10.5 17a6.5 6.5 0 1 0 0-13 6.5 6.5 0 0 0 0 13Zm4.7-1.8L21 21"/></svg><span>Search the library</span></button><span class="demo-shortcut"><kbd>⌘</kbd> <kbd>K</kbd></span></div>Contextual feedback with a title, detail, and supporting icon.
Your workspace is synced and ready.
This is a sample notice for your workspace.
import { Alert } from "@/components/sarahui/alert"; export function AlertDemo() { return ( <div className="flex flex-col gap-3"> <Alert title="All systems operational" description="Your workspace is synced and ready." /> <Alert title="Scheduled maintenance" description="Update queued for midnight." tone="warning" /> </div> );}
Add Alert to your project.
$ npx sarahui add alert
import { Alert } from 'sarahui';
<Alert title="Heads up!" description="You can add components to your app using the cli." />
Your workspace is synchronized.
import { Alert } from "@/components/sarahui/alert"; export function AlertInfo() { return ( <Alert tone="info" title="All systems operational"> Your workspace is synchronized. </Alert> );}
Workspace is at 88% capacity.
import { Alert } from "@/components/sarahui/alert"; export function AlertWarning() { return ( <Alert tone="warning" title="Approaching storage quota"> Workspace is at 88% capacity. </Alert> );}
Build error: syntax mismatch on line 42.
import { Alert } from "@/components/sarahui/alert"; export function AlertDanger() { return ( <Alert tone="danger" title="Deployment failed"> Build error encountered. </Alert> );}
Configuration parameters and properties for Alert.
| Prop | Type | Default | Description |
|---|---|---|---|
title |
string | required |
Alert headline |
description |
string | '' |
Supporting alert notice |
tone |
'brand' | 'success' | 'warning' | 'danger' | 'info' | 'brand' |
Visual semantic status tone |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Alert({ title: "Saved successfully", description: "Your changes are ready.", tone: "success", live: true }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-stack"><div class="s-alert s-tone-success"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4 12 5 5L20 6"/></svg><div><strong>All systems operational</strong><p>Your workspace is synced and ready.</p></div></div><div class="s-alert s-tone-warning"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m12 3 10 18H2Zm0 6v5m0 3h.01"/></svg><div><strong>A little heads-up</strong><p>This is a sample notice for your workspace.</p></div></div></div>Native modal focus containment, Escape, and focus restoration.
Your sample project appears in the Patterns workspace.
import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogDescription } from "@/components/sarahui/dialog";import { Button } from "@/components/sarahui/button"; export function DialogDemo() { return ( <Dialog> <DialogTrigger asChild> <Button variant="primary">Create project</Button> </DialogTrigger> <DialogContent> <DialogTitle>A new beginning</DialogTitle> <DialogDescription>Give your next idea a place to grow.</DialogDescription> </DialogContent> </Dialog> );}
Add Dialog to your project.
$ npx sarahui add dialog
import { Dialog } from 'sarahui';
<Dialog title="Edit profile" description="Make changes to your profile here."> <p>Dialog content</p></Dialog>
import { Dialog, DialogContent } from "@/components/sarahui/dialog"; export function DialogDemo() { return ( <Dialog trigger="Create project"> <DialogContent title="New project"> <p>Enter project details.</p> </DialogContent> </Dialog> );}
import { Dialog } from "@/components/sarahui/dialog"; export function DialogForm() { return <Dialog title="Workspace Preferences" formAction="/api/settings" />;}
Configuration parameters and properties for Dialog.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | required |
Unique dialog modal ID |
title |
string | required |
Dialog heading |
description |
string | '' |
Dialog accessible description |
children |
ReactNode | - |
Modal body content |
footer |
ReactNode | - |
Action button controls |
The example includes both the trigger and dialog. Escape closes it and returns focus.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.html`${UI.Button({ label: "Create project", attrs: { "data-s-dialog-open": "project-dialog" } })}${UI.Dialog({ id: "project-dialog", title: "Create a project", description: "Give your idea a name.", children: UI.Input({ id: "project-name", label: "Project name", attrs: { "data-s-autofocus": true } }), footer: UI.Button({ label: "Done", attrs: { "data-s-dialog-close": true } }) })}`; document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
The example includes both the trigger and dialog. Escape closes it and returns focus.
<div class="s-stack"><button type="button" data-s-dialog-open="create-project" class="s-button s-button--primary s-button--md "><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg><span>Create a project</span></button><p class="s-small s-muted">Your sample project appears in the Patterns workspace.</p></div>Short helper text on hover or focus, without obscuring content.
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/sarahui/tooltip";import { Button } from "@/components/sarahui/button"; export function TooltipDemo() { return ( <Tooltip> <TooltipTrigger asChild> <Button variant="outline">Hover over me</Button> </TooltipTrigger> <TooltipContent>Shared tokens keep your interface consistent.</TooltipContent> </Tooltip> );}
Add Tooltip to your project.
$ npx sarahui add tooltip
import { Tooltip } from 'sarahui';
<Tooltip text="Add to library"> <span>Hover me</span></Tooltip>
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/sarahui/tooltip"; export function TooltipDemo() { return ( <Tooltip> <TooltipTrigger asChild><Button variant="secondary">Hover Trigger</Button></TooltipTrigger> <TooltipContent>Add to personal workspace</TooltipContent> </Tooltip> );}
import { Tooltip, TooltipTrigger, TooltipContent } from "@/components/sarahui/tooltip"; export function TooltipShortcut() { return ( <Tooltip> <TooltipTrigger asChild><Button variant="outline">Search Shortcut</Button></TooltipTrigger> <TooltipContent>Command Palette <kbd>⌘K</kbd></TooltipContent> </Tooltip> );}
Configuration parameters and properties for Tooltip.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | required |
Accessible element ID |
label |
string | required |
Trigger anchor text |
text |
string | required |
Floating popover explanation |
Use short, supplementary text. Put essential instructions directly on the page.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Tooltip({ id: "token-help", label: "About design tokens", text: "Shared values keep your interface consistent." }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
Use short, supplementary text. Put essential instructions directly on the page.
<div class="s-row"><span class="s-small">A little help, close at hand</span><span class="s-tooltip" data-s-tooltip><button type="button" aria-describedby="demo-tooltip" class="s-button s-icon-button s-button--ghost s-button--md" aria-label="About tokens"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM12 11v6M12 7h.01"/></svg></button><span class="s-tooltip-bubble" id="demo-tooltip" role="tooltip">One change. A consistent system.</span></span></div>Polite announcements; the timer pauses on hover or focus.
import { Toast } from "@/components/sarahui/toast"; export default function ToastDemo() { return ( <div className="flex flex-col gap-3 w-full max-w-md p-6"> <Toast title="Changes saved" description="Your project configuration has been updated." tone="success" /> <Toast title="New update available" description="SarahUI v2.4 includes 319 Spectrum-aligned design tokens." tone="info" /> <Toast title="Deployment warning" description="Build cache was invalidated due to updated preset tokens." tone="warning" /> </div> );}
Add Toast to your project.
$ npx sarahui add toast
import { Toast } from 'sarahui';
toast("Event has been created.", { description: "Sunday, December 03, 2023 at 9:00 AM"})
import { toast } from "sarahui"; export function ToastSuccess() { return ( <button onClick={() => toast("Changes published live on edge!", { title: "Success" })}> Publish Changes </button> );}
import { toast } from "sarahui"; export function ToastWarning() { return ( <button onClick={() => toast("Network latency detected.", { title: "Warning", tone: "warning" })}> Simulate Warning </button> );}
Configuration parameters and properties for Toast.
| Prop | Type | Default | Description |
|---|---|---|---|
title |
string | '' |
Toast notification heading |
description |
string | required |
Announcement notification message |
tone |
'brand' | 'success' | 'warning' | 'danger' | 'info' | 'brand' |
Visual semantic tone |
Use toast() for a polite live announcement. It keeps focus where the person is working.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Button({ label: "Show notification", attrs: { id: "notify-button" } }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);document.querySelector('#notify-button').addEventListener('click', () => { UI.toast('Your changes are ready.', { title: 'Saved', tone: 'success' });});// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
Use toast() for a polite live announcement. It keeps focus where the person is working.
<div class="demo-wrap"><button type="button" data-feedback="Your changes are ready." class="s-button s-button--outline s-button--md "><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m4 12 5 5L20 6"/></svg><span>Show success</span></button><button type="button" data-feedback="This preview runs locally in your browser." data-tone="info" class="s-button s-button--ghost s-button--md "><span>Show notice</span></button></div>A sliding panel from the viewport edge for secondary content.
import { Drawer } from "@/components/sarahui/drawer";import { Button } from "@/components/sarahui/button"; export default function DrawerDemo() { return ( <div className="flex items-center justify-center p-8"> <Button variant="outline">Open Drawer</Button> <Drawer id="demo-drawer" title="Project Settings" description="Manage workspace preferences and deployment targets." position="right" > <div className="flex flex-col gap-4 py-4"> <p className="text-sm text-muted-foreground"> Configure project attributes, team access permissions, and automated build notifications. </p> <div className="flex justify-end gap-2 pt-4 border-t"> <Button variant="outline" size="sm">Cancel</Button> <Button variant="primary" size="sm">Save changes</Button> </div> </div> </Drawer> </div> );}
Add Drawer to your project.
$ npx sarahui add drawer
import { Drawer } from 'sarahui';
<Drawer title="Drawer Title" description="Drawer Description"> <div>Content</div></Drawer>
import { Drawer, DrawerTrigger, DrawerContent } from "@/components/sarahui/drawer"; export function DrawerDemo() { return ( <Drawer> <DrawerTrigger asChild><Button>Open Drawer</Button></DrawerTrigger> <DrawerContent> <h3>Edit Profile</h3> </DrawerContent> </Drawer> );}
import { Drawer, DrawerContent } from "@/components/sarahui/drawer"; export function DrawerBottom() { return <Drawer side="bottom"><DrawerContent>Payment Method</DrawerContent></Drawer>;}
Configuration parameters and properties for Drawer.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | '' |
Optional CSS class for styling overrides |
children |
ReactNode | - |
Children elements or content |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.html`${UI.Button({ label: "Open drawer", attrs: { "data-s-drawer-open": "app-drawer" } })}${UI.Drawer({ id: "app-drawer", title: "Drawer", children: "Drawer body content." })}`; document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-stack"><button type="button" data-s-drawer-open="demo-drawer" class="s-button s-button--primary s-button--md "><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"/></svg><span>Open side drawer</span></button><dialog id="demo-drawer" class="s-drawer s-drawer--right" aria-labelledby="demo-drawer-title" aria-describedby="demo-drawer-description"><header class="s-dialog-header"><div><h2 id="demo-drawer-title">Workspace Drawer</h2><p id="demo-drawer-description">Slide-over panel for rich configuration.</p></div><button type="button" data-s-dialog-close class="s-button s-icon-button s-button--ghost s-button--md" aria-label="Close drawer"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button></header><div class="s-dialog-body"><div class="s-stack"><p class="s-small s-muted">Custom drawer content with native focus containment.</p><button type="button" data-s-dialog-close class="s-button s-button--outline s-button--md "><span>Close drawer</span></button></div></div></dialog></div>Rich interactive content anchored directly to a trigger element.
Adjust contextual workspace preferences.
import { Popover } from "@/components/sarahui/popover";import { Button } from "@/components/sarahui/button"; export default function PopoverDemo() { return ( <div className="flex items-center justify-center p-8"> <Popover id="settings-popover" trigger={<Button variant="outline">Quick settings</Button>} content={ <div className="flex flex-col gap-2 p-2"> <h4 className="text-sm font-semibold text-foreground">Display Density</h4> <p className="text-xs text-muted-foreground">Choose compact or comfortable workspace layout.</p> <div className="mt-2 flex gap-2"> <Button size="sm" variant="outline">Compact</Button> <Button size="sm" variant="primary">Comfortable</Button> </div> </div> } /> </div> );}
Add Popover to your project.
$ npx sarahui add popover
import { Popover } from 'sarahui';
<Popover trigger={<Button>Open</Button>} content={<div>Popover Content</div>} />
import { Popover, PopoverTrigger, PopoverContent } from "@/components/sarahui/popover"; export function PopoverDemo() { return ( <Popover> <PopoverTrigger asChild><Button variant="outline">Dimensions</Button></PopoverTrigger> <PopoverContent>Width & Height controls</PopoverContent> </Popover> );}
Focus mode is disabled. Accepting pairing invites.
import { Popover, PopoverContent } from "@/components/sarahui/popover"; export function PopoverStatus() { return <Popover><PopoverContent>Online & Available</PopoverContent></Popover>;}
Configuration parameters and properties for Popover.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | '' |
Optional CSS class for styling overrides |
children |
ReactNode | - |
Children elements or content |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Popover({ id: "app-popover", trigger: UI.Button({ label: "Open popover" }), content: "Popover content." }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-popover" id="demo-popover" data-s-popover><button type="button" class="s-button s-button--outline s-button--md "><span>Open popover</span><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 9 7 7 7-7"/></svg></button><div class="s-popover-content" role="region" aria-label="Popover panel" hidden><div class="s-stack"><strong style="font-size:13px;">Quick Settings</strong><p class="s-small s-muted" style="margin:4px 0 10px;">Adjust contextual workspace preferences.</p><button type="button" class="s-button s-button--primary s-button--sm "><span>Confirm preferences</span></button></div></div></div>Extended side panel overlay for complex filtering and configuration.
import { Sheet } from "@/components/sarahui/sheet";import { Button } from "@/components/sarahui/button"; export default function SheetDemo() { return ( <div className="flex items-center justify-center p-8"> <Button variant="outline">Open Sheet</Button> <Sheet id="demo-sheet" title="Navigation Sheet" description="Quickly navigate across workspaces and recent designs." > <div className="flex flex-col gap-2 py-4"> <a href="#overview" className="rounded-lg px-3 py-2 text-sm font-medium hover:bg-muted">Overview</a> <a href="#components" className="rounded-lg px-3 py-2 text-sm font-medium hover:bg-muted">Components (120)</a> <a href="#foundations" className="rounded-lg px-3 py-2 text-sm font-medium hover:bg-muted">Design Tokens</a> <a href="#settings" className="rounded-lg px-3 py-2 text-sm font-medium hover:bg-muted">Settings</a> </div> </Sheet> </div> );}
Add Sheet to your project.
$ npx sarahui add sheet
import { Sheet } from 'sarahui';
<Sheet title="Edit profile" description="Make changes to your profile here."> <div>Sheet content</div></Sheet>
import { Sheet, SheetTrigger, SheetContent } from "@/components/sarahui/sheet"; export function SheetDemo() { return ( <Sheet> <SheetTrigger asChild><Button>Open Filters</Button></SheetTrigger> <SheetContent side="right">Filters checklist</SheetContent> </Sheet> );}
import { Sheet, SheetContent } from "@/components/sarahui/sheet"; export function SheetLeft() { return <Sheet><SheetContent side="left">Navigation menu</SheetContent></Sheet>;}
Configuration parameters and properties for Sheet.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | '' |
Optional CSS class for styling overrides |
children |
ReactNode | - |
Children elements or content |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.html`${UI.Button({ label: "Open Sheet", attrs: { "data-s-dialog-open": "app-sheet" } })}${UI.Sheet({ id: "app-sheet", title: "Navigation Sheet", children: "Sheet drawer contents." })}`; document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="demo-wrap"><button type="button" data-s-dialog-open="demo-sheet" class="s-button s-button--primary s-button--md "><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 12h16M4 18h16"/></svg><span>Open Navigation Sheet</span></button><dialog id="demo-sheet" class="s-dialog s-sheet s-sheet--right" aria-labelledby="demo-sheet-title" aria-describedby="demo-sheet-desc"><div class="s-dialog-backdrop" aria-hidden="true"></div><div class="s-sheet-card"><header class="s-dialog-header"><div><h2 id="demo-sheet-title" class="s-dialog-title">Quick Navigation</h2><p id="demo-sheet-desc" class="s-dialog-description">Slide-over panel for workspace navigation.</p></div><button type="button" class="s-button s-icon-button s-button--ghost s-button--sm" data-s-dialog-close aria-label="Close sheet"><svg class="s-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button></header><div class="s-sheet-body"><div class="s-stack"><a href="#overview" class="s-button s-button--ghost s-button--md">Overview</a><a href="#components" class="s-button s-button--ghost s-button--md">Components</a><a href="#foundations" class="s-button s-button--ghost s-button--md">Foundations</a></div></div></div></dialog></div>Context-sensitive action menu triggered on secondary click.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.ContextMenu({ id: "custom-ctx", trigger: UI.html`<div style="padding:20px;border:1px dashed var(--s-border-default);">Right click here</div>`, items: [{ label: "Edit", icon: "settings" }, { label: "Delete", icon: "trash", danger: true }] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="width:100%;"><div class="s-context-menu-wrap" data-s-context-menu="demo-contextmenu"><div style="padding:32px;text-align:center;border:1px dashed var(--s-border-default);border-radius:10px;background:var(--s-bg-surface);"><p style="margin:0;font-weight:500;">Right-click anywhere in this box</p><span class="s-small s-muted">Displays native contextual actions.</span></div><div id="demo-contextmenu" class="s-menu s-context-menu-panel" role="menu" hidden><button type="button" class="s-menu-item" role="menuitem" data-value="copy"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 15 6-6M7 13l-2 2a3.5 3.5 0 0 0 5 5l3-3m-2-10 2-2a3.5 3.5 0 0 1 5 5l-3 3"/></svg> <span>Copy link</span></button><button type="button" class="s-menu-item" role="menuitem" data-value="fav"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21 3.5 12.5a5.3 5.3 0 0 1 7.5-7.5l1 1 1-1a5.3 5.3 0 0 1 7.5 7.5Z"/></svg> <span>Save favorite</span></button><button type="button" class="s-menu-item s-menu-item--danger" role="menuitem" data-value="del"><svg class="s-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7m4-7v7"/></svg> <span>Delete item</span></button></div></div></div>Rich contextual preview popover appearing on element hover.
Thoughtful design system by MASARA X.
120 components · 0 depsimport { HoverCard } from "@/components/sarahui/hover-card";import { Button } from "@/components/sarahui/button";import { Avatar } from "@/components/sarahui/avatar"; export default function HoverCardDemo() { return ( <div className="flex items-center justify-center p-8"> <HoverCard trigger={<Button variant="outline" size="sm">@sarahui</Button>} content={ <div className="flex gap-4 p-2"> <Avatar name="SarahUI" size="md" tone="brand" /> <div className="flex flex-col"> <h4 className="text-sm font-semibold text-foreground">SarahUI</h4> <p className="text-xs text-muted-foreground">Framework-neutral design system by MASARA X.</p> <div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground"> <span>120 components</span> <span>•</span> <span>319 design tokens</span> </div> </div> </div> } /> </div> );}
Add HoverCard to your project.
$ npx sarahui add hover-card
import { HoverCard } from 'sarahui';
<HoverCard trigger={<Button variant="link">@nextjs</Button>}> <p>The React Framework – created and maintained by @vercel.</p></HoverCard>
Thoughtful design system by MASARA X.
120 components · 0 depsimport { HoverCard } from "@/components/sarahui/hover-card"; export function HoverCardUser() { return ( <HoverCard trigger="@sarahui"> <UserProfile name="Sarah Khan" role="Lead Designer" /> </HoverCard> );}
120 components, 319 design tokens, 0 runtime dependencies.
★ 4.9k stars on GitHubimport { HoverCard } from "@/components/sarahui/hover-card"; export function HoverCardLink() { return ( <HoverCard trigger="github.com/sarahui"> <RepoSummary name="masarax/sarahui" stars={4900} /> </HoverCard> );}
Configuration parameters and properties for HoverCard.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | '' |
Optional CSS class for styling overrides |
children |
ReactNode | - |
Children elements or content |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.HoverCard({ trigger: UI.Button({ label: "Hover over me" }), content: "Rich preview card information." }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="demo-wrap"><div class="s-hover-card-wrap" data-s-hover-card><div class="s-hover-card-trigger"><button type="button" class="s-button s-button--outline s-button--sm "><span>@sarahui</span></button></div><div class="s-hover-card-content" hidden><div style="display:flex;gap:12px;align-items:flex-start;"><span class="s-avatar s-avatar--md s-tone-brand" role="img" aria-label="Sarah Khan · Online">SK<i class="s-avatar-status" aria-hidden="true"></i></span><div><strong style="font-size:13px;">sarahUI</strong><p class="s-small s-muted" style="margin:2px 0 6px;">Thoughtful design system by MASARA X.</p><span class="s-small">120 components · 0 deps</span></div></div></div></div></div>A consistent stroke system. Decorative by default; name meaningful icons.
import { Icon } from "@/components/sarahui/icon"; export default function IconDemo() { return ( <div className="grid grid-cols-4 gap-6 p-6"> <div className="flex flex-col items-center gap-2 rounded-lg border p-3"> <Icon name="spark" size={24} label="Spark" /> <span className="text-xs text-muted-foreground">spark</span> </div> <div className="flex flex-col items-center gap-2 rounded-lg border p-3"> <Icon name="layers" size={24} label="Layers" /> <span className="text-xs text-muted-foreground">layers</span> </div> <div className="flex flex-col items-center gap-2 rounded-lg border p-3"> <Icon name="palette" size={24} label="Palette" /> <span className="text-xs text-muted-foreground">palette</span> </div> <div className="flex flex-col items-center gap-2 rounded-lg border p-3"> <Icon name="shield" size={24} label="Shield" /> <span className="text-xs text-muted-foreground">shield</span> </div> </div> );}
Add Icon to your project.
$ npx sarahui add icon
import { Icon } from 'sarahui';
<Icon name="spark" size={24} />
import { Icon } from "@/components/sarahui/icon"; export function IconGrid() { return ( <div> <Icon name="spark" size={24} /> <Icon name="layers" size={24} /> <Icon name="globe" size={24} /> </div> );}
import { Icon } from "@/components/sarahui/icon"; export function IconSizes() { return ( <> <Icon name="spark" size={16} /> <Icon name="spark" size={20} /> <Icon name="spark" size={24} /> <Icon name="spark" size={32} /> </> );}
Configuration parameters and properties for Icon.
| Prop | Type | Default | Description |
|---|---|---|---|
className |
string | '' |
Optional CSS class for styling overrides |
children |
ReactNode | - |
Children elements or content |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Icon({ name: "spark", size: 24, label: "Featured" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="icon-grid"><span title="spark"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="spark"><path d="m12 2 2.5 7.5L22 12l-7.5 2.5L12 22l-2.5-7.5L2 12l7.5-2.5Z"/></svg></span><span title="layers"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="layers"><path d="m12 3 10 5-10 5L2 8Zm-9 9 9 5 9-5m-18 5 9 5 9-5"/></svg></span><span title="palette"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="palette"><path d="M12 3a9 9 0 1 0 0 18h1a2 2 0 0 0 1.4-3.4 1.7 1.7 0 0 1 1.2-2.9H18a3 3 0 0 0 3-3.1A9 9 0 0 0 12 3ZM7 10h.01M10 6.5h.01M15 6.5h.01M18 10h.01"/></svg></span><span title="grid"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="grid"><path d="M3 3h7v7H3Zm11 0h7v7h-7ZM3 14h7v7H3Zm11 0h7v7h-7Z"/></svg></span><span title="globe"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="globe"><path d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM3 12h18M12 3c5 5 5 13 0 18-5-5-5-13 0-18Z"/></svg></span><span title="shield"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="shield"><path d="m12 2 9 4v6c0 5-9 10-9 10S3 17 3 12V6Zm-5 9 3 3 7-7"/></svg></span><span title="heart"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="heart"><path d="M12 21 3.5 12.5a5.3 5.3 0 0 1 7.5-7.5l1 1 1-1a5.3 5.3 0 0 1 7.5 7.5Z"/></svg></span><span title="folder"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="folder"><path d="M3 6h7l2 3h9v12H3Zm0 0V3h7l2 3h9v3"/></svg></span><span title="mail"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" role="img" aria-label="mail"><rect width="20" height="16" x="2" y="4" rx="2" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></svg></span><span title="calendar"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="calendar"><path d="M4 5h16v16H4ZM8 2v6m8-6v6M4 11h16"/></svg></span><span title="clock"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="clock"><path d="M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18ZM12 7v6l4 2"/></svg></span><span title="settings"><svg class="s-icon" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" role="img" aria-label="settings"><path d="M4 6h16M4 18h16M8 3v6m8 6v6M4 12h16M15 9v6"/></svg></span></div>Five visual priorities, three sizes, and thoughtful loading states.
import { Button } from "@/components/sarahui/button";import { ArrowRightIcon, PlusIcon } from "sarahui/icons"; export function ButtonDemo() { return ( <div className="flex flex-wrap gap-4 items-center"> <Button variant="primary" icon={<PlusIcon />}> Create project </Button> <Button variant="secondary">Secondary</Button> <Button variant="outline">Outline</Button> <Button variant="ghost">Ghost</Button> <Button variant="danger">Delete</Button> <Button variant="primary" trailing={<ArrowRightIcon />}> Get Started </Button> </div> );}
Add Button to your project.
$ npx sarahui add button
import { Button } from 'sarahui';
<Button variant="primary">Button</Button>
import { Button } from "@/components/sarahui/button"; export function ButtonPrimary() { return <Button variant="primary">Create project</Button>;}
import { Button } from "@/components/sarahui/button"; export function ButtonSecondary() { return <Button variant="secondary">Secondary action</Button>;}
import { Button } from "@/components/sarahui/button"; export function ButtonOutline() { return <Button variant="outline">Outline action</Button>;}
import { Button } from "@/components/sarahui/button"; export function ButtonGhost() { return <Button variant="ghost">Ghost action</Button>;}
import { Button } from "@/components/sarahui/button"; export function ButtonDanger() { return <Button variant="danger">Delete item</Button>;}
Configuration parameters and properties for Button.
| Prop | Type | Default | Description |
|---|---|---|---|
variant |
'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'primary' |
Visual hierarchy style of the button |
size |
'sm' | 'md' | 'lg' | 'md' |
Size and hit-target scale |
label |
string | '' |
Button text label |
icon |
string | '' |
Optional leading icon identifier |
disabled |
boolean | false |
Disables button interactions |
loading |
boolean | false |
Displays loading spinner and blocks repeat clicks |
Use attrs.type = "submit" inside a form. Loading buttons prevent repeated submission.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Button({ label: "Continue", icon: "arrow", variant: "primary", size: "md" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
Use attrs.type = "submit" inside a form. Loading buttons prevent repeated submission.
<div class="demo-wrap"><button type="button" data-s-dialog-open="create-project" class="s-button s-button--primary s-button--md "><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg><span>Create project</span></button><button type="button" data-feedback="Secondary action selected." class="s-button s-button--secondary s-button--md "><span>Secondary</span></button><button type="button" data-feedback="Outline action selected." class="s-button s-button--outline s-button--md "><span>Outline</span></button><button type="button" data-feedback="Ghost action selected." class="s-button s-button--ghost s-button--md "><span>Ghost</span></button><button type="button" data-feedback="This is a visual demo. Nothing was deleted." class="s-button s-button--danger s-button--md "><span>Delete</span></button></div>Compact actions, accessible names, and generous targets.
import { IconButton } from "@/components/sarahui/icon-button";import { PlusIcon, HeartIcon, CopyIcon, TrashIcon } from "sarahui/icons"; export function IconButtonDemo() { return ( <div className="flex gap-3 items-center"> <IconButton icon={<PlusIcon />} label="Create project" variant="primary" /> <IconButton icon={<HeartIcon />} label="Save to favorites" variant="outline" /> <IconButton icon={<CopyIcon />} label="Copy page link" variant="ghost" /> <IconButton icon={<TrashIcon />} label="Delete item" variant="danger" /> </div> );}
Add IconButton to your project.
$ npx sarahui add icon-button
import { IconButton } from 'sarahui';
<IconButton icon="heart" label="Save to favorites" variant="outline" />
import { IconButton } from "@/components/sarahui/icon-button"; export function IconButtonPrimary() { return <IconButton icon="plus" label="Create project" variant="primary" />;}
import { IconButton } from "@/components/sarahui/icon-button"; export function IconButtonFavorite() { return <IconButton icon="heart" label="Save to favorites" aria-pressed={false} />;}
Configuration parameters and properties for IconButton.
| Prop | Type | Default | Description |
|---|---|---|---|
icon |
string | required |
Icon identifier to render inside square button |
label |
string | required |
Accessible screen reader label |
variant |
'primary' | 'secondary' | 'outline' | 'ghost' | 'danger' | 'outline' |
Visual style variant |
size |
'sm' | 'md' | 'lg' | 'md' |
Touch target scale |
disabled |
boolean | false |
Disables user interactions |
Always provide a label that describes the action.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.IconButton({ icon: "heart", label: "Save to favorites", variant: "outline" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
Always provide a label that describes the action.
<div class="demo-wrap"><button type="button" data-s-dialog-open="create-project" class="s-button s-icon-button s-button--primary s-button--md" aria-label="Create project"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></button><button type="button" data-favorite aria-pressed="false" class="s-button s-icon-button s-button--outline s-button--md" aria-label="Save to favorites"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21 3.5 12.5a5.3 5.3 0 0 1 7.5-7.5l1 1 1-1a5.3 5.3 0 0 1 7.5 7.5Z"/></svg></button><button type="button" data-copy-page class="s-button s-icon-button s-button--ghost s-button--md" aria-label="Copy page link"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 8h12v13H9Zm-6 8V3h12"/></svg></button><button type="button" data-feedback="Visual demo action." class="s-button s-icon-button s-button--danger s-button--md" aria-label="Delete item"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M9 6V3h6v3M5 6l1 15h12l1-15M10 10v7m4-7v7"/></svg></button></div>Related actions grouped with individual focus and labels.
import { ButtonGroup } from "@/components/sarahui/button-group";import { Button } from "@/components/sarahui/button";import { GridIcon, CodeIcon, ShareIcon } from "sarahui/icons"; export function ButtonGroupDemo() { return ( <ButtonGroup label="Editor controls"> <Button variant="outline" icon={<GridIcon />}>Preview</Button> <Button variant="outline" icon={<CodeIcon />}>Code</Button> <Button variant="outline" icon={<ShareIcon />}>Share</Button> </ButtonGroup> );}
Add ButtonGroup to your project.
$ npx sarahui add button-group
import { ButtonGroup } from 'sarahui';
<ButtonGroup> <Button variant="outline">Preview</Button> <Button variant="outline">Code</Button></ButtonGroup>
import { ButtonGroup, Button } from "@/components/sarahui/button"; export function ButtonGroupDemo() { return ( <ButtonGroup> <Button variant="outline">Preview</Button> <Button variant="outline">Code</Button> <Button variant="outline">Share</Button> </ButtonGroup> );}
import { ButtonGroup, IconButton } from "@/components/sarahui/button"; export function ButtonGroupIcons() { return ( <ButtonGroup> <IconButton icon="grid" label="Grid" /> <IconButton icon="spark" label="Spark" /> <IconButton icon="settings" label="Settings" /> </ButtonGroup> );}
Configuration parameters and properties for ButtonGroup.
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string | required |
Accessible grouping role label |
children |
ReactNode[] | [] |
Array of connected button items |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.ButtonGroup({ label: "Editor actions", children: [UI.Button({ label: "Preview", variant: "outline" }), UI.Button({ label: "Code", variant: "outline" })] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-button-group" role="group" aria-label="Editor actions"><button type="button" data-feedback="Preview selected." class="s-button s-button--outline s-button--sm "><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3h7v7H3Zm11 0h7v7h-7ZM3 14h7v7H3Zm11 0h7v7h-7Z"/></svg><span>Preview</span></button><button type="button" data-feedback="Code selected." class="s-button s-button--outline s-button--sm "><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 6-6 6 6 6m8-12 6 6-6 6M14 3l-4 18"/></svg><span>Code</span></button><button type="button" data-copy-page class="s-button s-button--outline s-button--sm "><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m9 15 6-6M7 13l-2 2a3.5 3.5 0 0 0 5 5l3-3m-2-10 2-2a3.5 3.5 0 0 1 5 5l-3 3"/></svg><span>Share</span></button></div>Status communicated through text and color together.
import { Badge } from "@/components/sarahui/badge"; export function BadgeDemo() { return ( <div className="flex flex-wrap gap-2"> <Badge tone="brand">New release</Badge> <Badge tone="info">In progress</Badge> <Badge tone="success">Operational</Badge> <Badge tone="warning">In review</Badge> <Badge tone="danger">Needs attention</Badge> </div> );}
Add Badge to your project.
$ npx sarahui add badge
import { Badge } from 'sarahui';
<Badge variant="outline">Badge</Badge>
import { Badge } from "@/components/sarahui/badge"; export function BadgeTones() { return ( <> <Badge tone="brand">Brand</Badge> <Badge tone="success">Success</Badge> <Badge tone="warning">Warning</Badge> <Badge tone="danger">Danger</Badge> </> );}
import { Badge } from "@/components/sarahui/badge"; export function BadgeDot() { return <Badge tone="success" dot>Operational</Badge>;}
Configuration parameters and properties for Badge.
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string | required |
Badge display text |
tone |
'neutral' | 'brand' | 'success' | 'warning' | 'danger' | 'info' | 'neutral' |
Color tone |
dot |
boolean | false |
Shows status indicator dot |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Badge({ label: "In review", tone: "warning", dot: true }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="demo-wrap"><span class="s-badge s-tone-brand"><i class="s-status-dot" aria-hidden="true"></i>New release</span><span class="s-badge s-tone-info"><i class="s-status-dot" aria-hidden="true"></i>In progress</span><span class="s-badge s-tone-success"><i class="s-status-dot" aria-hidden="true"></i>Operational</span><span class="s-badge s-tone-warning"><i class="s-status-dot" aria-hidden="true"></i>In review</span><span class="s-badge s-tone-danger"><i class="s-status-dot" aria-hidden="true"></i>Needs attention</span><span class="s-badge s-tone-neutral"><i class="s-status-dot" aria-hidden="true"></i>Draft</span></div>Selected filters, categories, and removable choices.
import { Chip } from "@/components/sarahui/chip"; export default function ChipDemo() { return ( <div className="flex flex-wrap items-center gap-2 p-6"> <Chip label="Design Systems" selected={true} removable={true} /> <Chip label="Accessibility" selected={false} removable={true} /> <Chip label="Design Tokens" selected={true} removable={false} /> <Chip label="Zero Dependencies" selected={false} removable={false} /> </div> );}
Add Chip to your project.
$ npx sarahui add chip
import { Chip } from 'sarahui';
<Chip label="Design System" selected removable />
import { Chip } from "@/components/sarahui/chip"; export function ChipFilters() { return ( <div className="flex gap-2"> <Chip label="Design Systems" removable /> <Chip label="Accessibility" removable /> </div> );}
import { Chip } from "@/components/sarahui/chip"; export function ChipSelection() { return <Chip label="Selected Pill" selected />;}
Configuration parameters and properties for Chip.
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string | required |
Chip label |
removable |
boolean | false |
Shows dismissible close action |
selected |
boolean | false |
Active filter toggle state |
Removing a chip emits sarah:chipremove on its parent. Update your application state too.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Chip({ label: "Design system", selected: true, removable: true }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
Removing a chip emits sarah:chipremove on its parent. Update your application state too.
<div class="demo-wrap" aria-label="Selected tags" tabindex="-1"><span class="s-chip s-chip--selected">Design system<button type="button" data-s-chip-remove class="s-button s-icon-button s-button--ghost s-button--sm" aria-label="Remove Design system"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button></span><span class="s-chip ">Product<button type="button" data-s-chip-remove class="s-button s-icon-button s-button--ghost s-button--sm" aria-label="Remove Product"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button></span><span class="s-chip ">Engineering<button type="button" data-s-chip-remove class="s-button s-icon-button s-button--ghost s-button--sm" aria-label="Remove Engineering"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button></span><span class="s-chip ">Accessibility<button type="button" data-s-chip-remove class="s-button s-icon-button s-button--ghost s-button--sm" aria-label="Remove Accessibility"><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m6 6 12 12M18 6 6 18"/></svg></button></span><span class="s-chip ">Read-only badge</span></div>Privacy-friendly initials, sizes, and optional presence.
import { Avatar } from "@/components/sarahui/avatar"; export function AvatarDemo() { return ( <div className="flex gap-3 items-center"> <Avatar name="Sarah Khan" size="lg" status /> <Avatar name="Alex Morgan" tone="info" status /> <Avatar name="Jamie Kim" tone="success" /> <Avatar name="Maya Rahman" tone="warning" size="sm" /> </div> );}
Add Avatar to your project.
$ npx sarahui add avatar
import { Avatar } from 'sarahui';
<Avatar name="Sarah Khan" size="lg" />
import { Avatar } from "@/components/sarahui/avatar"; export function AvatarStatus() { return <Avatar name="Sarah Khan" status />;}
import { Avatar } from "@/components/sarahui/avatar"; export function AvatarSizes() { return ( <> <Avatar name="Sarah Khan" size="sm" /> <Avatar name="Sarah Khan" size="md" /> <Avatar name="Sarah Khan" size="lg" /> </> );}
Configuration parameters and properties for Avatar.
| Prop | Type | Default | Description |
|---|---|---|---|
name |
string | required |
User display name for monogram fallback |
size |
'sm' | 'md' | 'lg' | 'md' |
Avatar dimension scale |
tone |
string | 'brand' |
Color palette accent |
status |
boolean | false |
Online activity presence dot |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Avatar({ name: "Sarah Khan", size: "lg", tone: "brand", status: true }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="demo-wrap"><span class="s-avatar s-avatar--lg s-tone-brand" role="img" aria-label="Sarah Khan · Online">SK<i class="s-avatar-status" aria-hidden="true"></i></span><span class="s-avatar s-avatar--md s-tone-info" role="img" aria-label="Alex Morgan · Online">AM<i class="s-avatar-status" aria-hidden="true"></i></span><span class="s-avatar s-avatar--md s-tone-success" role="img" aria-label="Jamie Kim">JK</span><span class="s-avatar s-avatar--sm s-tone-warning" role="img" aria-label="Maya Rahman">MR</span></div>A quiet container with intentional spacing and hierarchy.
Bring your team and your next big thing together.
import { Card } from "@/components/sarahui/card";import { Button } from "@/components/sarahui/button";import { Progress } from "@/components/sarahui/progress"; export function CardDemo() { return ( <Card title="A place for your ideas" description="Bring your team and your next big thing together." footer={<Button variant="outline" size="sm">Explore workspace</Button>} > <Progress value={72} label="Workspace setup" /> </Card> );}
Add Card to your project.
$ npx sarahui add card
import { Card } from 'sarahui';
<Card> <CardHeader> <CardTitle>Card Title</CardTitle> <CardDescription>Card Description</CardDescription> </CardHeader> <CardContent> <p>Card Content</p> </CardContent></Card>
Bring your team and your next big thing together.
import { Card } from "@/components/sarahui/card"; export function CardDemo() { return ( <Card title="Workspace Overview" description="Manage your team."> <p>Card Body</p> </Card> );}
Review throughput and performance metrics.
import { Card } from "@/components/sarahui/card"; export function CardClickable() { return ( <Card isInteractive href="/analytics" title="Explore Analytics"> <p>Review metrics.</p> </Card> );}
Configuration parameters and properties for Card.
| Prop | Type | Default | Description |
|---|---|---|---|
title |
string | '' |
Card title |
description |
string | '' |
Card subtitle / description |
children |
ReactNode | - |
Main card content |
footer |
ReactNode | - |
Card footer actions |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Card({ title: "Your next idea", description: "Give it a place to grow.", children: UI.Progress({ label: "Setup", value: 72 }), footer: UI.Button({ label: "Continue", variant: "outline" }) }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<article class="s-card "><header class="s-card-header"><h3>A place for your ideas</h3><p>Bring your team and your next big thing together.</p></header><div class="s-card-body"><div class="s-progress-wrap"><div class="s-between s-small"><span>Workspace setup</span><strong>72%</strong></div><progress class="s-progress" value="72" max="100" aria-label="Workspace setup">72%</progress></div><div class="s-row card-people"><span class="s-avatar s-avatar--sm s-tone-brand" role="img" aria-label="Sarah Khan">SK</span><span class="s-avatar s-avatar--sm s-tone-info" role="img" aria-label="Alex Morgan">AM</span><span class="s-small s-muted">Made for working together</span></div></div><footer class="s-card-footer"><button type="button" data-go="#patterns" class="s-button s-button--outline s-button--sm "><span>Explore workspace</span><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"/></svg></button></footer></article>Semantic tables with numeric sorting and mixed row selection.
| Status | |||
|---|---|---|---|
| Studio website | In review | 92 | |
| Brand refresh | Active | 72 | |
| Mobile experience | Planning | 48 |
import { DataTable } from "@/components/sarahui/data-table"; export default function DataTableDemo() { return ( <div className="w-full max-w-2xl p-6"> <DataTable id="projects-table" caption="Team workspace projects" selectable={true} columns={[ { key: "name", label: "Project", sortable: true }, { key: "status", label: "Status", sortable: true }, { key: "progress", label: "Progress", sortable: true } ]} rows={[ { name: "Design System Audit", status: "Active", progress: "100%" }, { name: "Registry Migration", status: "Completed", progress: "94%" }, { name: "Token Compiler v2", status: "In Review", progress: "88%" }, { name: "E2E Test Hardening", status: "Testing", progress: "72%" } ]} /> </div> );}
Add DataTable to your project.
$ npx sarahui add data-table
import { DataTable } from 'sarahui';
<DataTable columns={[ { key: "name", label: "Name" }, { key: "status", label: "Status" }]} rows={[ { id: "1", name: "Project Alpha", status: "Active" }]} />
| Status | |||
|---|---|---|---|
| Studio website | In review | 92 | |
| Brand refresh | Active | 72 | |
| Mobile experience | Planning | 48 |
import { DataTable } from "@/components/sarahui/data-table"; export function DataTableDemo() { return ( <DataTable columns={[ { key: "name", label: "Project", sortable: true }, { key: "status", label: "Status" } ]} rows={projects} /> );}
| Status | |||
|---|---|---|---|
| Studio website | In review | 92 | |
| Brand refresh | Active | 72 | |
| Mobile experience | Planning | 48 |
import { DataTable } from "@/components/sarahui/data-table"; export function DataTableSelectable() { return <DataTable selectable columns={cols} rows={data} />;}
Configuration parameters and properties for DataTable.
| Prop | Type | Default | Description |
|---|---|---|---|
id |
string | required |
Table container identifier |
caption |
string | '' |
Accessible screen reader title |
columns |
Array<{ key: string, label: string, sortable?: boolean }> | [] |
Column headers |
rows |
Array<Record<string, any>> | [] |
Data row records |
selectable |
boolean | false |
Row checkbox selection |
Rows keep stable IDs. Sorting operates on rendered rows; sort application data before paginating.
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.DataTable({ id: "projects", caption: "Project progress", columns: [{ key: "name", label: "Project", sortable: true }, { key: "progress", label: "Progress (%)", sortable: true }], rows: [{ id: "p1", name: "Brand refresh", progress: 72 }, { id: "p2", name: "Studio website", progress: 92 }], selectable: true }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
Rows keep stable IDs. Sorting operates on rendered rows; sort application data before paginating.
<div class="s-table-scroll" tabindex="0" role="region" aria-label="Example projects table"><table class="s-table" id="demo-datatable" data-s-table><caption class="s-sr-only">Example projects</caption><thead><tr><th scope="col"><label class="s-choice s-choice--checkbox" for="demo-datatable-all"><input data-s-select-all id="demo-datatable-all" type="checkbox" class="s-choice-input s-checkbox"><span class="s-choice-copy"><span>Select all rows</span></span></label></th><th scope="col"><button type="button" data-s-sort="name">Project<svg class="s-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 9 7 7 7-7"/></svg></button></th><th scope="col">Status</th><th scope="col"><button type="button" data-s-sort="progress">Progress (%)<svg class="s-icon" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m5 9 7 7 7-7"/></svg></button></th></tr></thead><tbody><tr data-row="0"><td><label class="s-choice s-choice--checkbox" for="demo-datatable-row-0"><input data-s-row-select id="demo-datatable-row-0" type="checkbox" class="s-choice-input s-checkbox"><span class="s-choice-copy"><span>Select Studio website</span></span></label></td><td data-column="name" data-sort="Studio website">Studio website</td><td data-column="status"><span class="s-badge s-tone-warning"><i class="s-status-dot" aria-hidden="true"></i>In review</span></td><td data-column="progress" data-sort="92">92</td></tr><tr data-row="1"><td><label class="s-choice s-choice--checkbox" for="demo-datatable-row-1"><input data-s-row-select id="demo-datatable-row-1" type="checkbox" class="s-choice-input s-checkbox"><span class="s-choice-copy"><span>Select Brand refresh</span></span></label></td><td data-column="name" data-sort="Brand refresh">Brand refresh</td><td data-column="status"><span class="s-badge s-tone-success"><i class="s-status-dot" aria-hidden="true"></i>Active</span></td><td data-column="progress" data-sort="72">72</td></tr><tr data-row="2"><td><label class="s-choice s-choice--checkbox" for="demo-datatable-row-2"><input data-s-row-select id="demo-datatable-row-2" type="checkbox" class="s-choice-input s-checkbox"><span class="s-choice-copy"><span>Select Mobile experience</span></span></label></td><td data-column="name" data-sort="Mobile experience">Mobile experience</td><td data-column="status"><span class="s-badge s-tone-info"><i class="s-status-dot" aria-hidden="true"></i>Planning</span></td><td data-column="progress" data-sort="48">48</td></tr></tbody></table></div>Progressive disclosure using native details and summary.
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/sarahui/accordion"; export function AccordionDemo() { return ( <Accordion type="single" collapsible defaultValue="item-1"> <AccordionItem value="item-1"> <AccordionTrigger>Can I make this my own?</AccordionTrigger> <AccordionContent>Adjust the tokens and every component adapts automatically.</AccordionContent> </AccordionItem> <AccordionItem value="item-2"> <AccordionTrigger>Does it support dark mode?</AccordionTrigger> <AccordionContent>Every semantic color has an explicit light and dark value.</AccordionContent> </AccordionItem> </Accordion> );}
Add Accordion to your project.
$ npx sarahui add accordion
import { Accordion } from 'sarahui';
<Accordion items={[ { title: "Is it accessible?", content: "Yes. It adheres to the WAI-ARIA design pattern." }]} />
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/sarahui/accordion"; export function AccordionDemo() { return ( <Accordion type="single" collapsible> <AccordionItem value="item-1"> <AccordionTrigger>Can I customize source tokens?</AccordionTrigger> <AccordionContent>Yes! Customize all tokens in src/styles.css.</AccordionContent> </AccordionItem> </Accordion> );}
import { Accordion } from "@/components/sarahui/accordion"; export function AccordionMultiple() { return <Accordion type="multiple" items={items} />;}
Configuration parameters and properties for Accordion.
| Prop | Type | Default | Description |
|---|---|---|---|
items |
Array<{ title: string, content: string | ReactNode, open?: boolean }> | [] |
Collapsible accordion panels |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Accordion({ items: [{ title: "Does this support dark mode?", content: "Every semantic color has a light and dark value.", open: true }, { title: "Can I customize it?", content: "Override CSS variables in your application." }] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-accordion"><details open><summary>Can I make this my own?<svg class="s-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></summary><div>Adjust the source tokens, rebuild, and every component follows your direction.</div></details><details><summary>Does it support dark mode?<svg class="s-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></summary><div>Every semantic color has an explicit light and dark alias.</div></details><details><summary>Do I need a framework?<svg class="s-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg></summary><div>No. The core uses native HTML, CSS variables, and ES modules.</div></details></div>Native determinate progress with a visible label and value.
import { Progress } from "@/components/sarahui/progress"; export function ProgressDemo() { return ( <div className="w-full max-w-sm flex flex-col gap-4"> <Progress value={72} label="Design system progress" /> <Progress value={100} label="Foundations complete" /> </div> );}
Add Progress to your project.
$ npx sarahui add progress
import { Progress } from 'sarahui';
<Progress value={33} />
import { Progress } from "@/components/sarahui/progress"; export function ProgressDemo() { return <Progress value={72} label="Uploading assets" />;}
import { Progress } from "@/components/sarahui/progress"; export function ProgressIndeterminate() { return <Progress indeterminate label="Processing query" />;}
Configuration parameters and properties for Progress.
| Prop | Type | Default | Description |
|---|---|---|---|
value |
number | 0 |
Current progress percentage (0 - 100) |
label |
string | '' |
Accessible progress bar label |
showLabel |
boolean | true |
Displays percentage text |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Progress({ label: "Workspace setup", value: 72, showLabel: true }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-stack"><div class="s-progress-wrap"><div class="s-between s-small"><span>Design system progress</span><strong>72%</strong></div><progress class="s-progress" value="72" max="100" aria-label="Design system progress">72%</progress></div><div class="s-progress-wrap"><div class="s-between s-small"><span>Foundations complete</span><strong>100%</strong></div><progress class="s-progress" value="100" max="100" aria-label="Foundations complete">100%</progress></div></div>Calm loading placeholders that respect reduced motion.
import { Skeleton } from "@/components/sarahui/skeleton"; export default function SkeletonDemo() { return ( <div className="flex flex-col gap-4 w-full max-w-sm rounded-xl border border-border bg-card p-6 shadow-sm"> <div className="flex items-center gap-4"> <Skeleton className="size-12 rounded-full" /> <div className="flex flex-col gap-2 flex-1"> <Skeleton className="h-4 w-3/4 rounded" /> <Skeleton className="h-3 w-1/2 rounded" /> </div> </div> <Skeleton lines={3} label="Loading project feed" /> </div> );}
Add Skeleton to your project.
$ npx sarahui add skeleton
import { Skeleton } from 'sarahui';
<Skeleton className="h-4 w-[250px]" />
import { Skeleton } from "@/components/sarahui/skeleton"; export function SkeletonDemo() { return <Skeleton lines={4} />;}
import { Skeleton } from "@/components/sarahui/skeleton"; export function SkeletonCard() { return ( <div className="flex gap-3 items-center"> <Skeleton className="w-10 h-10 rounded-full" /> <div className="space-y-2 flex-1"> <Skeleton className="h-3 w-3/4" /> <Skeleton className="h-2 w-1/2" /> </div> </div> );}
Configuration parameters and properties for Skeleton.
| Prop | Type | Default | Description |
|---|---|---|---|
lines |
number | 3 |
Number of animated skeleton placeholders |
label |
string | 'Loading content' |
Screen reader accessibility announcement |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Skeleton({ lines: 4, label: "Loading project details" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-skeleton-group" role="status" aria-label="Loading project details"><span class="s-skeleton" aria-hidden="true"></span><span class="s-skeleton" aria-hidden="true"></span><span class="s-skeleton" aria-hidden="true"></span><span class="s-skeleton" aria-hidden="true"></span></div>A helpful explanation and a useful next step.
A blank canvas. A world of possibilities.
import { EmptyState } from "@/components/sarahui/empty-state";import { Button } from "@/components/sarahui/button"; export default function EmptyStateDemo() { return ( <div className="flex items-center justify-center p-8"> <EmptyState icon="folder" title="No projects yet" description="Your next idea starts here. Create a project to begin building." action={<Button variant="primary" size="sm">Create project</Button>} /> </div> );}
Add EmptyState to your project.
$ npx sarahui add empty-state
import { EmptyState } from 'sarahui';
<EmptyState title="No projects found" description="Get started by creating a new project." />
A blank canvas. A world of possibilities.
import { EmptyState } from "@/components/sarahui/empty-state"; export function EmptyStateDemo() { return ( <EmptyState title="Your next idea starts here" description="Create a project to begin." actionLabel="Create project" /> );}
Try clearing your active filters or searching another keyword.
import { EmptyState } from "@/components/sarahui/empty-state"; export function EmptyStateSearch() { return ( <EmptyState title="No records found" description="Try clearing your active search filter." actionLabel="Clear filters" /> );}
Configuration parameters and properties for EmptyState.
| Prop | Type | Default | Description |
|---|---|---|---|
title |
string | required |
Headline |
description |
string | '' |
Supporting guidance |
action |
ReactNode | - |
Call to action button |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.EmptyState({ title: "No projects yet", description: "Your next idea starts here.", icon: "folder", action: UI.Button({ label: "Create project", icon: "plus" }) }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-empty-state"><span><svg class="s-icon" width="27" height="27" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h7l2 3h9v12H3Zm0 0V3h7l2 3h9v3"/></svg></span><h3>Your next idea starts here</h3><p>A blank canvas. A world of possibilities.</p><button type="button" data-s-dialog-open="create-project" class="s-button s-button--primary s-button--sm "><svg class="s-icon" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg><span>Create project</span></button></div>A subtle pause between groups, optionally labeled.
A little breathing room between ideas.
import { Separator } from "@/components/sarahui/separator"; export default function SeparatorDemo() { return ( <div className="flex flex-col gap-4 w-full max-w-sm p-6 text-sm"> <div> <h4 className="font-semibold text-foreground">SarahUI Components</h4> <p className="text-muted-foreground">120 modular zero-dependency UI elements.</p> </div> <Separator /> <div> <h4 className="font-semibold text-foreground">Adobe Spectrum Tokens</h4> <p className="text-muted-foreground">319 3-tier semantic tokens.</p> </div> <Separator label="Or continue with" /> <div className="flex justify-center"> <span className="text-xs text-muted-foreground">Native web components</span> </div> </div> );}
Add Separator to your project.
$ npx sarahui add separator
import { Separator } from 'sarahui';
<Separator />
A little breathing room between ideas.
import { Separator } from "@/components/sarahui/separator"; export function SeparatorDemo() { return <Separator />;}
import { Separator } from "@/components/sarahui/separator"; export function SeparatorLabeled() { return <Separator label="OR CONTINUE WITH" />;}
Configuration parameters and properties for Separator.
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string | '' |
Optional centered divider label |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Separator({ label: "Or continue with" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-stack"><p class="s-small s-muted">A little breathing room between ideas.</p><hr class="s-separator"><div class="s-separator-label"><span>Or continue with</span></div></div>Named loading indicators and a decorative button option.
import { Spinner } from "@/components/sarahui/spinner"; export default function SpinnerDemo() { return ( <div className="flex items-center justify-center gap-6 p-8"> <div className="flex items-center gap-3"> <Spinner label="Loading workspace data" /> <span className="text-sm text-muted-foreground">Loading workspace...</span> </div> <div className="flex items-center gap-3"> <Spinner decorative={true} /> <span className="text-sm font-medium">Syncing tokens...</span> </div> </div> );}
Add Spinner to your project.
$ npx sarahui add spinner
import { Spinner } from 'sarahui';
<Spinner label="Loading..." />
import { Spinner } from "@/components/sarahui/spinner"; export function SpinnerSizes() { return ( <> <Spinner size="sm" /> <Spinner size="md" /> <Spinner size="lg" /> </> );}
import { Spinner } from "@/components/sarahui/spinner"; export function SpinnerAccessible() { return <Spinner label="Syncing workspace..." />;}
Configuration parameters and properties for Spinner.
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string | 'Loading' |
Accessible screen reader label |
decorative |
boolean | false |
Hides from assistive technology if redundant |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Spinner({ label: "Syncing workspace" }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-row" style="gap:12px;align-items:center;"><span class="s-spinner" role="status" aria-label="Syncing workspace"></span><span class="s-small s-muted">Syncing your workspace…</span></div>A two-state button communicating pressed states.
import { Toggle } from "@/components/sarahui/toggle"; export default function ToggleDemo() { return ( <div className="flex items-center gap-3 p-6"> <Toggle label="Bookmark" icon="heart" pressed={true} /> <Toggle label="Mute notifications" icon="settings" pressed={false} /> <Toggle label="Favorite" icon="spark" pressed={true} size="sm" /> </div> );}
Add Toggle to your project.
$ npx sarahui add toggle
import { Toggle } from 'sarahui';
<Toggle aria-label="Toggle italic"> <Italic className="h-4 w-4" /></Toggle>
import { Toggle } from "@/components/sarahui/toggle"; export function ToggleDemo() { return <Toggle icon="heart" label="Bookmark" defaultPressed />;}
import { Toggle } from "@/components/sarahui/toggle"; export function ToggleMute() { return <Toggle icon="settings" label="Mute notifications" />;}
Configuration parameters and properties for Toggle.
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string | required |
Accessible button label |
icon |
string | '' |
Icon glyph |
pressed |
boolean | false |
Aria pressed state |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Toggle({ label: "Bookmark", icon: "heart", pressed: true }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="demo-wrap"><button type="button" class="s-toggle s-toggle--md" aria-pressed="true" data-s-toggle><svg class="s-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 21 3.5 12.5a5.3 5.3 0 0 1 7.5-7.5l1 1 1-1a5.3 5.3 0 0 1 7.5 7.5Z"/></svg><span>Bookmark</span></button><button type="button" class="s-toggle s-toggle--md" aria-pressed="false" data-s-toggle><svg class="s-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M4 6h16M4 18h16M8 3v6m8 6v6M4 12h16M15 9v6"/></svg><span>Mute notifications</span></button></div>Segmented group of two-state buttons for single or multiple choice.
import { ToggleGroup } from "@/components/sarahui/toggle-group"; export default function ToggleGroupDemo() { return ( <div className="flex flex-col items-center gap-4 p-6"> <ToggleGroup label="Text alignment" type="single" items={[ { label: "Left", icon: "arrow", pressed: true }, { label: "Center", icon: "grid", pressed: false }, { label: "Right", icon: "arrow", pressed: false } ]} /> </div> );}
Add ToggleGroup to your project.
$ npx sarahui add toggle-group
import { ToggleGroup } from 'sarahui';
<ToggleGroup type="single" items={[ { label: "Bold", value: "bold" }, { label: "Italic", value: "italic" }]} />
import { ToggleGroup, ToggleGroupItem } from "@/components/sarahui/toggle-group"; export function ToggleGroupDemo() { return ( <ToggleGroup type="single" defaultValue="center"> <ToggleGroupItem value="left">Left</ToggleGroupItem> <ToggleGroupItem value="center">Center</ToggleGroupItem> <ToggleGroupItem value="right">Right</ToggleGroupItem> </ToggleGroup> );}
import { ToggleGroup, ToggleGroupItem } from "@/components/sarahui/toggle-group"; export function ToggleGroupMultiple() { return ( <ToggleGroup type="multiple"> <ToggleGroupItem value="bold">Bold</ToggleGroupItem> <ToggleGroupItem value="italic">Italic</ToggleGroupItem> </ToggleGroup> );}
Configuration parameters and properties for ToggleGroup.
| Prop | Type | Default | Description |
|---|---|---|---|
label |
string | required |
Accessible group label |
items |
Array<{ label: string, icon?: string, pressed?: boolean }> | [] |
Array of connected toggle options |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.ToggleGroup({ label: "Alignment", items: [{ label: "Left", pressed: true }, { label: "Center" }, { label: "Right" }] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-toggle-group" role="group" aria-label="Alignment controls" data-s-toggle-group="single"><button type="button" class="s-toggle s-toggle--md" aria-pressed="true" data-s-toggle><svg class="s-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"/></svg><span>Left</span></button><button type="button" class="s-toggle s-toggle--md" aria-pressed="false" data-s-toggle><svg class="s-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 3h7v7H3Zm11 0h7v7h-7ZM3 14h7v7H3Zm11 0h7v7h-7Z"/></svg><span>Center</span></button><button type="button" class="s-toggle s-toggle--md" aria-pressed="false" data-s-toggle><svg class="s-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M5 12h14m-6-6 6 6-6 6"/></svg><span>Right</span></button></div>Displays keyboard keys and shortcuts with physical keycap depth.
import { Kbd } from "@/components/sarahui/kbd"; export default function KbdDemo() { return ( <div className="flex flex-wrap items-center justify-center gap-4 p-8"> <div className="flex items-center gap-2"> <span className="text-sm text-muted-foreground">Search:</span> <Kbd keys={["⌘", "K"]} /> </div> <div className="flex items-center gap-2"> <span className="text-sm text-muted-foreground">Command palette:</span> <Kbd keys={["Ctrl", "Shift", "P"]} /> </div> <div className="flex items-center gap-2"> <span className="text-sm text-muted-foreground">Close:</span> <Kbd keys={["Esc"]} /> </div> </div> );}
Add Kbd to your project.
$ npx sarahui add kbd
import { Kbd } from 'sarahui';
<Kbd keys={["⌘", "K"]} />
import { Kbd } from "@/components/sarahui/kbd"; export function KbdDemo() { return ( <div className="flex gap-1"> <Kbd>⌘</Kbd> <Kbd>K</Kbd> </div> );}
import { Kbd } from "@/components/sarahui/kbd"; export function KbdEscape() { return <Kbd>Esc</Kbd>;}
Configuration parameters and properties for Kbd.
| Prop | Type | Default | Description |
|---|---|---|---|
keys |
string[] | [] |
Array of keyboard glyphs (e.g. ["⌘", "K"]) |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Kbd({ keys: ["⌘", "K"] }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="demo-wrap" style="align-items:center;"><span class="s-kbd s-kbd--md"><kbd>⌘</kbd><kbd>K</kbd></span><span class="s-kbd s-kbd--md"><kbd>Ctrl</kbd><kbd>Shift</kbd><kbd>P</kbd></span><span class="s-kbd s-kbd--md"><kbd>Esc</kbd></span></div>Guarantees responsive aspect ratios for media and interactive embeds.
import { AspectRatio } from "@/components/sarahui/aspect-ratio"; export default function AspectRatioDemo() { return ( <div className="w-full max-w-md p-6"> <AspectRatio ratio="16/9" className="overflow-hidden rounded-xl border border-border bg-muted"> <div className="flex h-full w-full flex-col items-center justify-center bg-gradient-to-br from-primary/20 to-primary/5 p-4 text-center"> <span className="text-base font-semibold text-foreground">16:9 Aspect Ratio</span> <p className="mt-1 text-xs text-muted-foreground">Maintains responsive proportions across all viewport widths.</p> </div> </AspectRatio> </div> );}
Add AspectRatio to your project.
$ npx sarahui add aspect-ratio
import { AspectRatio } from 'sarahui';
<AspectRatio ratio={16 / 9}> <img src="..." alt="Image" className="rounded-md object-cover" /></AspectRatio>
import { AspectRatio } from "@/components/sarahui/aspect-ratio"; export function AspectRatioDemo() { return ( <AspectRatio ratio={16 / 9}> <img src="/hero.jpg" alt="Hero" className="object-cover w-full h-full" /> </AspectRatio> );}
import { AspectRatio } from "@/components/sarahui/aspect-ratio"; export function AspectRatioSquare() { return ( <AspectRatio ratio={1 / 1}> <img src="/avatar.jpg" alt="Profile" className="object-cover" /> </AspectRatio> );}
Configuration parameters and properties for AspectRatio.
| Prop | Type | Default | Description |
|---|---|---|---|
ratio |
string | '16/9' |
Aspect ratio proportions (e.g. 16/9, 4/3, 1/1) |
children |
ReactNode | - |
Media or canvas content constrained by ratio |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.AspectRatio({ ratio: "16/9", children: UI.html`<div style="background:var(--s-bg-brand);width:100%;height:100%;"></div>` }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div style="max-width:320px;width:100%;"><div class="s-aspect-ratio" style="aspect-ratio:16/9"><div class="s-aspect-content" style="background:var(--s-bg-brand);color:var(--s-text-brand);display:grid;place-items:center;border-radius:8px;font-size:13px;font-weight:600;">16:9 Video Canvas</div></div></div>Smooth animated disclosure container with accessible state.
import { Collapsible } from "@/components/sarahui/collapsible"; export default function CollapsibleDemo() { return ( <div className="flex flex-col gap-3 w-full max-w-md p-6"> <Collapsible title="Can I use SarahUI with any framework?" content="Yes! SarahUI is framework-neutral and built on web standard templates with zero runtime dependencies." open={true} /> <Collapsible title="How are design tokens structured?" content="Tokens follow Adobe Spectrum 3-tier taxonomy: Global tokens, Semantic tokens, and Component-scoped tokens." open={false} /> </div> );}
Add Collapsible to your project.
$ npx sarahui add collapsible
import { Collapsible } from 'sarahui';
<Collapsible title="Can I use this with any framework?"> Yes! sarahUI uses standard Web APIs.</Collapsible>
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/sarahui/collapsible"; export function CollapsibleDemo() { return ( <Collapsible defaultOpen> <CollapsibleTrigger>What is SarahUI?</CollapsibleTrigger> <CollapsibleContent>SarahUI is a modern design system.</CollapsibleContent> </Collapsible> );}
import { Collapsible } from "@/components/sarahui/collapsible"; export function CollapsibleNested() { return ( <div className="space-y-2"> <Collapsible title="Is it compatible with Next.js?" /> <Collapsible title="Are there any dependencies?" /> </div> );}
Configuration parameters and properties for Collapsible.
| Prop | Type | Default | Description |
|---|---|---|---|
title |
string | 'Can I use this with any framework?' |
Summary label rendered in the disclosure trigger |
content |
string | '' |
Body text revealed when the disclosure is open |
open |
boolean | false |
Renders the disclosure expanded by default |
attrs |
Record<string, any> | {} |
Extra HTML attributes forwarded to the root element |
Add <div id="app"></div> to your HTML, then use this JavaScript entry.
import 'sarahui/css';import * as UI from 'sarahui'; const component = UI.Collapsible({ title: "Can I use this with any framework?", content: "Yes! sarahUI uses standard Web APIs and plain HTML." }); document.querySelector('#app').innerHTML = UI.render( UI.html`<main class="s-ui">${component}</main>`);const cleanup = UI.enhanceUI(document);// Call cleanup() when your application unmounts.Complete parameters detailed in Props.
<div class="s-stack"><details class="s-collapsible" open><summary class="s-collapsible-trigger"><span>Can I use sarahUI with React, Vue, or Next.js?</span><svg class="s-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 4 8 8-8 8"/></svg></summary><div class="s-collapsible-content"><div class="s-collapsible-inner">Yes! sarahUI uses standard Web APIs and plain HTML templates, making it 100% compatible with any frontend framework.</div></div></details><details class="s-collapsible"><summary class="s-collapsible-trigger"><span>Are there any external dependencies?</span><svg class="s-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="m8 4 8 8-8 8"/></svg></summary><div class="s-collapsible-content"><div class="s-collapsible-inner">Zero runtime dependencies. Pure CSS variables and native browser APIs.</div></div></details></div>Try another name or show all categories.
03 / IN GOOD COMPANY
Thoughtful starting points for real interfaces. Explore a workspace and a settings experience.
Interactive examples with sample data. Changes stay in this browser session.
YOUR WORKSPACE
Here’s what’s taking shape in your studio.
A comfortable form that helps people feel in control.
Small choices that make your workspace feel like you.
Account settingsHow to install dependencies, configure your application, and start building with 120 free components.
Pure vanilla ES modules, native CSS variables, and standard Web APIs.
Special effects, text animations, device mocks, backgrounds, and layout frames.
Complete type definitions (.d.ts) bundled with zero compilation warnings.
319 design tokens benchmarked against Adobe Spectrum Design Data.
Add individual components via CLI (npx sarahui add <component>) or install the complete sarahui package using your preferred package manager. No external peer dependencies are required.
pnpm add sarahuinpm install sarahuiyarn add sarahuibun add sarahuiImport sarahui/css once at the root of your application. This includes all design tokens, animation keyframes, and bundled local fonts.
import 'sarahui/css';Import components and call render() to generate safe, escaped HTML markup. Call enhanceUI(root) to attach interactive behaviors.
import 'sarahui/css';import { Card, Button, render, enhanceUI, toast } from 'sarahui'; const app = document.querySelector('#app');app.innerHTML = render( Card({ title: 'Welcome to sarahUI', description: '120 free and open-source animated components, effects, and widgets.', footer: Button({ label: 'Get Started', tone: 'primary', attrs: { id: 'hello-button' } }) })); const cleanup = enhanceUI(app);document.querySelector('#hello-button').addEventListener('click', () => { toast('Welcome to sarahUI! 120 components ready.');});// Call cleanup() when tearing down the containersarahUI works seamlessly across modern front-end frameworks and full-stack setups. Select your stack below:
import 'sarahui/css';import { Card, Button, render, enhanceUI, toast } from 'sarahui'; const app = document.querySelector('#app');app.innerHTML = render( Card({ title: 'Welcome to sarahUI', description: '120 free and open-source animated components, effects, and widgets.', footer: Button({ label: 'Get Started', tone: 'primary', attrs: { id: 'hello-button' } }) })); const cleanup = enhanceUI(app);document.querySelector('#hello-button').addEventListener('click', () => { toast('Welcome to sarahUI! 120 components ready.');});// Call cleanup() when tearing down the containerTemplates return escaped HTML. Run enhanceUI after mounting and dispose before unmounting.
import 'sarahui/css';import { useEffect, useRef } from 'react';import { enhanceUI } from 'sarahui'; export default function App() { const containerRef = useRef(null); useEffect(() => { if (containerRef.current) { return enhanceUI(containerRef.current); } }, []); return ( <main className="s-ui min-h-screen flex items-center justify-center"> <button type="button" className="s-button s-button--primary s-button--md" onClick={() => console.log('Action triggered')} > <span>Explore Components</span> </button> </main> );}Use your framework to own application state. The template functions are not framework-specific components.
<script setup>import 'sarahui/css';import { ref, onMounted, onBeforeUnmount } from 'vue';import { enhanceUI } from 'sarahui'; const root = ref(null);const enabled = ref(true);let cleanup = () => {}; onMounted(() => { if (root.value) cleanup = enhanceUI(root.value);});onBeforeUnmount(() => cleanup());</script> <template> <main ref="root" class="s-ui"> <label class="s-choice"> <input v-model="enabled" type="checkbox" class="s-choice-input s-switch" role="switch"> <span>Enable ambient motion</span> </label> </main></template>Use your framework to own application state. The template functions are not framework-specific components.
// resources/js/app.js — imported through your Vite pipelineimport 'sarahui/css';import { enhanceUI } from 'sarahui'; enhanceUI(document); // In your Blade layout:// @vite(['resources/js/app.js'])//// <main class="s-ui">// <button type="button" class="s-button s-button--primary s-button--md">// <span>Deploy Application</span>// </button>// </main>Use your framework to own application state. The template functions are not framework-specific components.
Toggle between light and dark modes with setTheme(), or override standard CSS variables to match your brand:
import { setTheme } from 'sarahui'; // Options: 'light' | 'dark' | 'system'setTheme('dark');:root { --s-action-primary: #5B3DF5; --s-card-radius: 16px;} /* Scoped container override */.my-section { --s-card-padding: 32px;}SYSTEM EVENTS
Components emit standard bubbling CustomEvents that you can listen for anywhere in your DOM hierarchy.
| Event | event.detail | Trigger Condition |
|---|---|---|
sarah:themechange | preference, theme | Triggered when theme changes between light and dark |
sarah:tabchange | index, id | Triggered when an active tab changes |
sarah:selectionchange | selected, count | Triggered when a table row is selected or deselected |
sarah:sortchange | key, direction | Triggered when a table sort control is activated |
sarah:pagechange | page, pages | Triggered when a pagination item is clicked |
sarah:chipremove | label | Triggered when a removable chip is dismissed |
sarah:menuaction | value | Triggered when a menu item is selected |
sarah:command | href, label | Triggered when a command palette item is activated |
FREQUENTLY ASKED QUESTIONS