Variants & styling
Varsel keeps visuals opinionated yet flexible. Choose a preset variant for status-driven colors and stack your own classes on top for bespoke experiences.
Variant property
Set the variant property when invoking toast. Available values are default, success, warning, destructive and info.
<script lang="ts">
import { toast } from "varsel";
const variants = ["default", "success", "warning", "destructive", "info"] as const;
</script>
<div class="flex flex-wrap gap-3">
{#each variants as variant}
<button
class="rounded-md bg-card border border-border h-9 px-4 py-2 text-sm font-medium text-foreground hover:bg-card-muted shadow-sm transition-[background-color,scale] duration-150 ease-out active:scale-[0.975]"
onclick={() =>
toast({
title: `${variant} toast`,
description: "Pick the tone that matches the event.",
variant,
})}
>
{variant}
</button>
{/each}
</div>Design tokens
Varsel’s default look ships from the bundled stylesheet, but everything is powered by a small set of CSS variables. Instead of importing the package CSS you can copy the token definitions below into your own theme file and tweak every hue, shadow and easing curve.
@import "tailwindcss";
@source "./**/*.{svelte,js,ts}";
:root {
/* Base hue used for generating the color palette (oklch) */
--base-hue: 265;
/* Semantic color tokens */
--color-vs-popover: oklch(1 0 0);
--color-vs-popover-muted: oklch(0.96 0.002 var(--base-hue));
--color-vs-foreground: oklch(0.1408 0.0044 var(--base-hue));
--color-vs-border: oklch(0.8925 0.0014 var(--base-hue));
--color-vs-ring: oklch(0.55 0.012 var(--base-hue));
--color-vs-ring-offset: oklch(0.96 0.002 var(--base-hue));
--color-vs-destructive: oklch(0.62 0.21 25);
--color-vs-warning: oklch(0.8 0.2 75);
--color-vs-success: oklch(0.7 0.18 155);
--color-vs-info: oklch(0.7 0.18 240);
/* Shadows */
--shadow-vs-button:
0px 1px 1px -0.5px rgba(0, 0, 0, 0.15), 0px 3px 3px -1.5px
rgba(0, 0, 0, 0.05);
--shadow-vs-toast:
0px 1px 1px -0.5px rgba(0, 0, 0, 0.15),
0px 3px 3px -1.5px rgba(0, 0, 0, 0.05),
0px 6px 6px -3px rgba(0, 0, 0, 0.05),
0px 12px 12px -6px rgba(0, 0, 0, 0.05),
0px 24px 24px -12px rgba(0, 0, 0, 0.05),
0px 48px 48px -24px rgba(0, 0, 0, 0.05);
/* Radius & Easing */
--radius-base: 0.125rem;
--ease-vs-button: cubic-bezier(0.25, 0.46, 0.45, 0.94);
--ease-vs-toast: cubic-bezier(0.32, 0.72, 0, 1);
--ease-vs-pop: cubic-bezier(0.18, 0.89, 0.32, 1.28);
}
.dark {
--color-vs-popover: oklch(0.2139 0.0101 var(--base-hue));
--color-vs-popover-muted: oklch(0.2502 0.016 var(--base-hue));
--color-vs-foreground: oklch(0.9824 0.0013 var(--base-hue));
--color-vs-border: oklch(0.278 0.015 var(--base-hue));
--color-vs-ring: oklch(0.58 0.012 var(--base-hue));
--color-vs-ring-offset: oklch(0.15 0.005 var(--base-hue));
--color-vs-destructive: oklch(0.72 0.27 25);
--color-vs-warning: oklch(0.82 0.24 85);
--color-vs-success: oklch(0.78 0.25 155);
--color-vs-info: oklch(0.78 0.18 240);
}
@theme {
--color-vs-popover: var(--vs-popover);
--color-vs-popover-muted: var(--vs-popover-muted);
--color-vs-foreground: var(--vs-foreground);
--color-vs-border: var(--vs-border);
--color-vs-ring: var(--vs-ring);
--color-vs-ring-offset: var(--vs-ring-offset);
--color-vs-destructive: var(--vs-destructive);
--color-vs-warning: var(--vs-warning);
--color-vs-success: var(--vs-success);
--color-vs-info: var(--vs-info);
--shadow-vs-button: var(--shadow-vs-button);
--shadow-vs-toast: var(--shadow-vs-toast);
--radius-vs-sm: calc(var(--radius-base) * 2);
--radius-vs-md: calc(var(--radius-base) * 3);
--radius-vs-lg: calc(var(--radius-base) * 4);
--ease-vs-button: var(--ease-vs-button);
--ease-vs-toast: var(--ease-vs-toast);
--ease-vs-pop: var(--ease-vs-pop);
}
/* Spinner Styles */
.vs-spinner {
display: inline-flex;
width: 1rem;
height: 1rem;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: absolute;
inset: 0;
transform-origin: center;
}
.vs-spinner--active {
opacity: 1;
}
.vs-spinner--finish {
animation: vs-spinner-finish 0.42s var(--ease-vs-toast) forwards;
}
.vs-spinner svg {
width: 100%;
height: 100%;
animation: vs-spin 1s linear infinite;
}
/* Icon Styles */
.vs-icon {
display: inline-flex;
width: 1rem;
height: 1rem;
align-items: center;
justify-content: center;
flex-shrink: 0;
position: absolute;
inset: 0;
}
.vs-icon--static {
opacity: 1;
transform: scale(1);
}
.vs-icon--waiting {
opacity: 0;
transform: scale(0.75) rotate(-6deg);
}
.vs-icon--pop {
animation: vs-icon-pop 0.36s var(--ease-vs-pop) forwards;
}
.vs-icon svg {
width: 100%;
height: 100%;
}
@keyframes vs-spin {
to {
transform: rotate(360deg);
}
}
@keyframes vs-spinner-finish {
0% {
opacity: 1;
transform: scale(1) rotate(0deg);
filter: blur(0);
}
60% {
opacity: 0.65;
transform: scale(0.55) rotate(140deg);
filter: blur(0.3px);
}
100% {
opacity: 0;
transform: scale(0) rotate(220deg);
filter: blur(0.8px);
}
}
@keyframes vs-icon-pop {
0% {
opacity: 0;
transform: scale(0.5) rotate(-10deg);
filter: blur(2px);
}
60% {
opacity: 1;
transform: scale(1.18) rotate(2deg);
filter: blur(0);
}
100% {
opacity: 1;
transform: scale(1) rotate(0deg);
filter: blur(0);
}
}
@media (prefers-reduced-motion: reduce) {
.vs-spinner svg {
animation-duration: 2s;
}
.vs-spinner--finish {
animation-duration: 0s;
}
.vs-icon--pop {
animation: none;
opacity: 1;
transform: scale(1);
}
}Override any of these values (for example swap the hue or easing curves) to give Varsel a custom feel while keeping the component API the same. If you do include the packaged CSS, your overrides still win thanks to normal cascade rules.