Skip to content
Textlab
1
Browse

Tokens

Components read a small set of tokens instead of raw values, so one change restyles the whole library in both themes.

Color

Semantic colors fall back to shadcn's variables when your app defines them. Effect tokens drive highlights, glows and spectrums.

--tl-background
--tl-surface
--tl-foreground
--tl-text-secondary
--tl-accent
--tl-fx-highlight
--tl-fx-glow
--tl-fx-spectrum-1
--tl-fx-spectrum-2
--tl-fx-spectrum-3
--tl-fx-spectrum-4

Easing

Entrances use ease-out so text arrives fast and lands softly. Things already on screen use a symmetric curve.

Duration, spring and stagger

duration.instant0.1s
duration.fast0.16s
duration.base0.24s
duration.slow0.4s
duration.reveal0.8s
duration.ambient2.4s
spring.snappy0.25s, bounce 0
spring.smooth0.4s, bounce 0.1
spring.bouncy0.5s, bounce 0.3
spring.gentle0.8s, bounce 0.1
stagger.char0.02s
stagger.word0.06s
stagger.line0.1s

Interface feedback stays under 300 ms. Text entrances can take longer, but opacity always leads so words are readable before the motion settles.

The stylesheet

tokens.css
/*
 * Textlab tokens. Import once in your root layout:
 *   import "@/components/textlab/tokens.css";
 * Colors fall back to shadcn's semantic variables when they exist.
 */
:root {
  --tl-background: var(--background, oklch(0.985 0 0));
  --tl-surface: oklch(0.965 0 0);
  --tl-surface-2: oklch(0.935 0 0);
  --tl-foreground: var(--foreground, oklch(0.17 0 0));
  --tl-text-secondary: oklch(0.42 0 0);
  --tl-text-tertiary: oklch(0.56 0 0);
  --tl-border: var(--border, oklch(0.9 0 0));
  --tl-accent: oklch(0.52 0.16 148);
  --tl-accent-foreground: oklch(0.99 0 0);

  --tl-fx-highlight: oklch(0.92 0.15 100 / 0.75);
  --tl-fx-glow: oklch(0.7 0.19 145);
  --tl-fx-ink: oklch(0.2 0.02 260);
  --tl-fx-spectrum-1: oklch(0.7 0.19 30);
  --tl-fx-spectrum-2: oklch(0.78 0.16 80);
  --tl-fx-spectrum-3: oklch(0.7 0.14 190);
  --tl-fx-spectrum-4: oklch(0.6 0.2 290);

  --tl-font-sans: var(--font-sans, ui-sans-serif, system-ui, sans-serif);
  --tl-font-display: var(--font-display, ui-sans-serif, system-ui, sans-serif);
  --tl-font-flex: var(--font-flex, var(--tl-font-sans));
  --tl-font-mono: var(--font-mono, ui-monospace, SFMono-Regular, monospace);
  --tl-font-script: var(--font-script, "Snell Roundhand", "Brush Script MT", cursive);
  --tl-text-display: clamp(2.5rem, 6vw + 1rem, 6rem);
  --tl-text-title: clamp(1.75rem, 3vw + 1rem, 3rem);
  --tl-leading-tight: 1.05;
  --tl-tracking-tight: -0.02em;

  --tl-ease-enter: cubic-bezier(0.23, 1, 0.32, 1);
  --tl-ease-exit: cubic-bezier(0.23, 1, 0.32, 1);
  --tl-ease-standard: cubic-bezier(0.77, 0, 0.175, 1);
  --tl-ease-emphasized: cubic-bezier(0.16, 1, 0.3, 1);
  --tl-duration-fast: 160ms;
  --tl-duration-base: 240ms;
  --tl-duration-slow: 400ms;
  --tl-duration-reveal: 800ms;
}

.dark,
[data-theme="dark"] {
  --tl-background: var(--background, oklch(0.145 0 0));
  --tl-surface: oklch(0.185 0 0);
  --tl-surface-2: oklch(0.23 0 0);
  --tl-foreground: var(--foreground, oklch(0.97 0 0));
  --tl-text-secondary: oklch(0.74 0 0);
  --tl-text-tertiary: oklch(0.6 0 0);
  --tl-border: var(--border, oklch(0.28 0 0));
  --tl-accent: oklch(0.86 0.2 132);
  --tl-accent-foreground: oklch(0.16 0 0);

  --tl-fx-highlight: oklch(0.55 0.13 100 / 0.55);
  --tl-fx-glow: oklch(0.86 0.2 132);
  --tl-fx-ink: oklch(0.92 0.01 260);
}

/* Parts rendered hidden on the server show anyway if scripts are slow. Hydration removes data-tl-pending. */
[data-tl-pending] [data-tl-part] {
  animation: tl-settle 0.4s var(--tl-ease-enter) 2.4s forwards;
}

@keyframes tl-settle {
  to {
    opacity: 1;
    transform: none;
    filter: none;
    clip-path: none;
    --tl-wipe: 200%;
  }
}

/* Before hydration the JS reduced-motion branch has not run yet, so the stylesheet shows the final state. */
@media (prefers-reduced-motion: reduce) {
  [data-tl-motion="user"] [data-tl-part] {
    opacity: 1 !important;
    transform: none !important;
    filter: none !important;
    clip-path: none !important;
    mask-image: none !important;
    -webkit-mask-image: none !important;
    animation: none !important;
  }
}