v0.3.0
demo

components

Same DOM in static HTML or React. Each component lists both. Pick the tab that matches your stack.

Mark

The brand glyph. SVG, scales with size, colors via currentColor.

<svg class="sn-mark sn-mark--lg" viewBox="0 0 32 32" fill="currentColor" aria-hidden="true">
  <!-- crystal paths… -->
</svg>

<!-- pre-built sprite -->
<svg class="sn-mark sn-mark--lg"><use href="/icons/marks.svg#crystal"/></svg>
import { MarkCrystal } from "@snowztech/ui";

<MarkCrystal size={48} />
<MarkCrystal size="lg" />
<MarkCrystal size={64} color="var(--sn-accent)" />

Seven other variants live in the logo lab.

Monogram

Wraps any mark in a tile. App icons, brand stamps.

<span class="sn-monogram sn-monogram--md">
  <svg class="sn-mark sn-mark--md" viewBox="0 0 32 32"><!-- mark --></svg>
</span>

<span class="sn-monogram sn-monogram--inverse">
  <svg class="sn-mark sn-mark--md"><!-- mark --></svg>
</span>
import { Monogram, MarkCrystal } from "@snowztech/ui";

<Monogram size={64} mark={MarkCrystal} />
<Monogram size={64} mark={MarkCrystal} bg="#f7f7f5" fg="#161616" radius={10} />

Avatar

Image with initials fallback. Circle or square. Optional ring.

<span class="sn-avatar sn-avatar--lg">DR</span>
<span class="sn-avatar sn-avatar--lg sn-avatar--square">DR</span>
<span class="sn-avatar sn-avatar--lg sn-avatar--ring">DR</span>

<span class="sn-avatar sn-avatar--md">
  <img src="/me.jpg" alt="">
</span>

<span class="sn-avatar-stack">
  <span class="sn-avatar sn-avatar--md sn-avatar--ring">A</span>
  <span class="sn-avatar sn-avatar--md sn-avatar--ring">B</span>
  <span class="sn-avatar sn-avatar--md sn-avatar--ring">C</span>
</span>
import { Avatar, AvatarStack } from "@snowztech/ui";

<Avatar size={48} name="DR" />
<Avatar size={48} src="/me.jpg" name="DR" />
<Avatar size={48} name="DR" shape="square" ring />

<AvatarStack overlap={10}>
  <Avatar size={32} name="Ada" ring />
  <Avatar size={32} name="Bo" ring />
</AvatarStack>

Wordmark

snowztech.
<span class="sn-wordmark" style="--sn-wordmark-size: 24px">
  snowztech<span class="sn-wordmark__dot">.</span>
</span>
import { SnowWordmark } from "@snowztech/ui";

<SnowWordmark size={24} />

Pulse

Status dot with breathing animation.

onlinedegradeddown
<span class="sn-pulse sn-pulse--success">
  <span class="sn-pulse__dot"></span>
  <span>online</span>
</span>

<span class="sn-pulse sn-pulse--warning">
  <span class="sn-pulse__dot"></span>
  <span>degraded</span>
</span>
import { Pulse } from "@snowztech/ui";

<Pulse label="online" />
<Pulse color="var(--sn-warning)" label="degraded" />
<Pulse color="var(--sn-danger)" label="down" />

Buttons

.sn-btn + a variant. Sizes via --sm / --lg.

{/* variants */}
<button className="sn-btn sn-btn--primary">Primary</button>
<button className="sn-btn sn-btn--secondary">Secondary</button>
<button className="sn-btn sn-btn--ghost">Ghost</button>
<button className="sn-btn sn-btn--accent">Accent</button>
<button className="sn-btn sn-btn--danger">Danger</button>

{/* sizes */}
<button className="sn-btn sn-btn--primary sn-btn--sm">Small</button>
<button className="sn-btn sn-btn--primary sn-btn--lg">Large</button>

Badges

defaultaccentokwarnerr
<span className="sn-badge">default</span>
<span className="sn-badge sn-badge--accent">accent</span>
<span className="sn-badge sn-badge--success">ok</span>
<span className="sn-badge sn-badge--warning">warn</span>
<span className="sn-badge sn-badge--danger">err</span>

Input

Single-line text input. Sizes sm / md / lg. Pass invalid for error state.

<input class="sn-input" placeholder="Search…" />
<input class="sn-input sn-input--sm" placeholder="small" />
<input class="sn-input sn-input--lg" placeholder="large" />
<input class="sn-input sn-input--invalid" value="not-an-email" />
<input class="sn-input" disabled placeholder="disabled" />
import { Input } from "@snowztech/ui";

<Input placeholder="Search…" />
<Input size="sm" placeholder="small" />
<Input size="lg" placeholder="large" />
<Input invalid defaultValue="not-an-email" />
<Input disabled placeholder="disabled" />

Textarea

Multi-line text. Resize vertical by default.

<textarea class="sn-textarea" placeholder="Paste your text here…"></textarea>
<textarea class="sn-textarea sn-textarea--invalid">boom</textarea>
import { Textarea } from "@snowztech/ui";

<Textarea placeholder="Paste your text here…" />
<Textarea invalid defaultValue="boom" />

File input

Styled via ::file-selector-button — no JS, native input. Works in React too: <input type="file" className="sn-file" />.

<input type="file" className="sn-file" />
<input type="file" className="sn-file" accept=".pdf" />
<input type="file" className="sn-file" disabled />

Field

Label + control + hint/error in one row. Use Field for forms; drop to Label if you need to compose manually.

We never share it.
Already taken.
<div class="sn-field">
  <label class="sn-label" for="email">Email<span class="sn-label__required">*</span></label>
  <input id="email" class="sn-input" type="email" placeholder="you@snowztech.com" />
  <span class="sn-hint">We never share it.</span>
</div>

<div class="sn-field">
  <label class="sn-label" for="user">Username</label>
  <input id="user" class="sn-input sn-input--invalid" value="lucas" />
  <span class="sn-error" role="alert">Already taken.</span>
</div>
import { Field, Input, Textarea } from "@snowztech/ui";

<Field label="Email" hint="We never share it." htmlFor="email" required>
  <Input id="email" type="email" placeholder="you@snowztech.com" />
</Field>

<Field label="Bio" htmlFor="bio">
  <Textarea id="bio" placeholder="A few words…" />
</Field>

<Field label="Username" htmlFor="user" error="Already taken.">
  <Input id="user" invalid defaultValue="lucas" />
</Field>

Theme toggle

Tiny button that flips data-theme on <html> and persists to localStorage. React version is a client component.

<button class="sn-theme-toggle" data-sn-theme-toggle aria-label="toggle theme">
  <span class="sn-theme-toggle__indicator"></span>
</button>

<!-- auto-wires [data-sn-theme-toggle] on load -->
<script type="module"
        src="https://cdn.jsdelivr.net/npm/@snowztech/ui-js/dist/snowztech-ui.esm.js"></script>
import { ThemeToggle } from "@snowztech/ui/client";

<ThemeToggle />
<ThemeToggle size={16} defaultTheme="dark" storageKey="theme" />

Card

Flat by default. Compose with --rounded, --accent, --inset.

Default
One border. Flat. Square corners.
Rounded
Bigger radius for friendlier surfaces.
Accent
Use sparingly to draw attention.
Inset + headerok
Recessed background. Pairs with header rows for stat tiles.
<div className="sn-card">…</div>
<div className="sn-card sn-card--rounded">…</div>
<div className="sn-card sn-card--accent">…</div>
<div className="sn-card sn-card--inset">…</div>

{/* compose */}
<div className="sn-card sn-card--rounded sn-card--accent">…</div>