Skip to content
Velocity X Docs
← All components

Readability Kit

Free · MIT

Four long-form scannability primitives — Callout (4 variants), a Key Takeaways TL;DR box, a large PullQuote and a reading-time pill. Branded via CSS custom properties, dark-mode aware, zero motion, zero dependencies.

Category
Long-form
Framework
astro
Version
1.0.1
Peer deps
none

Install

bash
velocityx add readability-kit

Free and MIT licensed — no account needed. The file lands under src/components/velocity/readability-kit/ by default (override with --dest), and it's yours to edit from the moment it's written.

Demo

View the live demo on the showcase site ↗

Source

Pulled live from the registry — this is the exact file velocityx add readability-kit writes into your project right now.

components/velocity/readability-kit/Callout.astro

astro
---
//    VELOCITY COMPONENT · theming
// 
//    Colours here are Tailwind `primary-*` / `accent-*`, which resolve through
//    the Velocity preset. Out of the box they are a neutral indigo — NOT the
//    Aidxn brand — so this drops into your project without dragging our look in.
// 
//    ⚡ NOT USING VELOCITY? Run this once, in your project root:
// 
//          velocityx theme
// 
//    It writes velocity-preset.cjs + velocity-theme.css. Add the preset to your
//    tailwind.config, import the css, then set your whole brand in ONE place:
// 
//          :root {
//            --vx-brand: custom;
//            --vx-primary-600: 220 38 38;   <- your action colour
//            --vx-primary-700: 185 28 28;
//          }
// 
//    ⚠️  Raw RGB channels, space separated — NOT hex. `#DC2626` here silently
//        breaks every /opacity variant in the library (bg-primary-600/20 etc).
// 
//    Prefer not to install anything? Find-and-replace `primary-` with your own
//    scale name. Nothing else in this file depends on Velocity.
/**
 * Callout — Velocity Growth Layer · Readability Kit
 * ────────────────────────────────────────────────────────────────────────────
 * A branded inline aside that breaks long-form copy into a scannable block.
 * Four variants (info / tip / warning / success), each with its own icon +
 * accent tint. Renders as a semantic <aside role="note"> so screen readers
 * announce it as supplementary content. Dark-mode + reduced-motion friendly
 * (no motion here — purely presentational).
 *
 * Usage:
 *   <Callout variant="tip" title="Pro tip">
 *     Lead with the takeaway, then back it up.
 *   </Callout>
 */
export interface Props {
  variant?: 'info' | 'tip' | 'warning' | 'success';
  title?: string;
}

const { variant = 'info', title } = Astro.props;

const VARIANTS = {
  info: {
    label: 'Note',
    box: 'border-primary-500/20 dark:border-primary-300/20 bg-primary-50/70 dark:bg-primary-500/10',
    icon: 'text-primary-600 dark:text-primary-300 bg-primary-500/10 dark:bg-primary-400/15',
    title: 'text-primary-900 dark:text-primary-100',
    path: 'M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z',
  },
  tip: {
    label: 'Tip',
    box: 'border-cyan-500/20 dark:border-cyan-300/20 bg-cyan-50/70 dark:bg-cyan-500/10',
    icon: 'text-cyan-600 dark:text-cyan-300 bg-cyan-500/10 dark:bg-cyan-400/15',
    title: 'text-cyan-900 dark:text-cyan-100',
    path: 'M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 7.478a12.06 12.06 0 01-4.5 0m3.75 2.383a14.406 14.406 0 01-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 10-7.517 0c.85.493 1.509 1.333 1.509 2.316V18',
  },
  warning: {
    label: 'Heads up',
    box: 'border-orange-500/25 dark:border-orange-300/25 bg-orange-50/70 dark:bg-orange-500/10',
    icon: 'text-orange-600 dark:text-orange-300 bg-orange-500/10 dark:bg-orange-400/15',
    title: 'text-orange-900 dark:text-orange-100',
    path: 'M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z',
  },
  success: {
    label: 'Done right',
    box: 'border-green-500/20 dark:border-green-300/20 bg-green-50/70 dark:bg-green-500/10',
    icon: 'text-green-600 dark:text-green-300 bg-green-500/10 dark:bg-green-400/15',
    title: 'text-green-900 dark:text-green-100',
    path: 'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z',
  },
} as const;

const v = VARIANTS[variant];
const heading = title ?? v.label;
---
<aside
  role="note"
  aria-label={heading}
  class:list={['not-prose my-6 flex gap-3.5 rounded-2xl border p-4 sm:p-5', v.box]}
>
  <span
    aria-hidden="true"
    class:list={['mt-0.5 inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-xl', v.icon]}
  >
    <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8">
      <path stroke-linecap="round" stroke-linejoin="round" d={v.path} />
    </svg>
  </span>
  <div class="min-w-0 flex-1">
    <p class:list={['m-0 text-sm font-semibold tracking-tight', v.title]}>{heading}</p>
    <div class="mt-1 text-sm leading-relaxed text-secondary-700 dark:text-secondary-300 [&_a]:text-primary-600 dark:[&_a]:text-primary-300 [&_p]:m-0 [&_p+p]:mt-2">
      <slot />
    </div>
  </div>
</aside>

components/velocity/readability-kit/KeyTakeaways.astro

astro
---
//    VELOCITY COMPONENT · theming
// 
//    Colours here are Tailwind `primary-*` / `accent-*`, which resolve through
//    the Velocity preset. Out of the box they are a neutral indigo — NOT the
//    Aidxn brand — so this drops into your project without dragging our look in.
// 
//    ⚡ NOT USING VELOCITY? Run this once, in your project root:
// 
//          velocityx theme
// 
//    It writes velocity-preset.cjs + velocity-theme.css. Add the preset to your
//    tailwind.config, import the css, then set your whole brand in ONE place:
// 
//          :root {
//            --vx-brand: custom;
//            --vx-primary-600: 220 38 38;   <- your action colour
//            --vx-primary-700: 185 28 28;
//          }
// 
//    ⚠️  Raw RGB channels, space separated — NOT hex. `#DC2626` here silently
//        breaks every /opacity variant in the library (bg-primary-600/20 etc).
// 
//    Prefer not to install anything? Find-and-replace `primary-` with your own
//    scale name. Nothing else in this file depends on Velocity.
/**
 * KeyTakeaways — Velocity Growth Layer · Readability Kit
 * ────────────────────────────────────────────────────────────────────────────
 * A boxed "Key takeaways" TL;DR list with check bullets. Drop it at the top of
 * a long post so scanners get the payload in five seconds. Pass `items` as a
 * string[] OR slot your own <li> list (slot wins if both are given).
 *
 * Renders as a labelled <section> with a real <ul> so it's announced as a list
 * with N items. Purely presentational — dark-mode aware, no motion.
 *
 * Usage:
 *   <KeyTakeaways items={["First point", "Second point"]} />
 *   <KeyTakeaways><li>Custom markup</li></KeyTakeaways>
 */
export interface Props {
  items?: string[];
  title?: string;
}

const { items = [], title = 'Key takeaways' } = Astro.props;
const hasSlot = Astro.slots.has('default');
const headingId = `kt-${Math.random().toString(36).slice(2, 8)}`;

const CHECK =
  'M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z';
---
<section
  aria-labelledby={headingId}
  class="not-prose my-6 rounded-2xl border border-primary-500/15 dark:border-primary-300/15 bg-gradient-to-br from-primary-50/80 to-white dark:from-primary-500/10 dark:to-secondary-950/40 p-5 sm:p-6"
>
  <p
    id={headingId}
    class="m-0 flex items-center gap-2 text-xs font-semibold uppercase tracking-[0.14em] text-primary-700 dark:text-primary-300"
  >
    <svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2" aria-hidden="true">
      <path stroke-linecap="round" stroke-linejoin="round" d="M3.75 12h16.5m-16.5 5.25h16.5M3.75 6.75h16.5" />
    </svg>
    {title}
  </p>

  {hasSlot ? (
    <ul class="mt-3 space-y-2.5 [&>li]:flex [&>li]:gap-2.5 [&>li]:text-sm [&>li]:leading-relaxed [&>li]:text-secondary-800 dark:[&>li]:text-secondary-200 [&>li]:list-none">
      <slot />
    </ul>
  ) : (
    <ul class="mt-3 list-none space-y-2.5 p-0">
      {items.map((item) => (
        <li class="flex gap-2.5 text-sm leading-relaxed text-secondary-800 dark:text-secondary-200">
          <svg
            class="mt-0.5 h-4 w-4 shrink-0 text-primary-600 dark:text-primary-300"
            fill="none"
            viewBox="0 0 24 24"
            stroke="currentColor"
            stroke-width="2"
            aria-hidden="true"
          >
            <path stroke-linecap="round" stroke-linejoin="round" d={CHECK} />
          </svg>
          <span>{item}</span>
        </li>
      ))}
    </ul>
  )}
</section>

components/velocity/readability-kit/PullQuote.astro

astro
---
//    VELOCITY COMPONENT · theming
// 
//    Colours here are Tailwind `primary-*` / `accent-*`, which resolve through
//    the Velocity preset. Out of the box they are a neutral indigo — NOT the
//    Aidxn brand — so this drops into your project without dragging our look in.
// 
//    ⚡ NOT USING VELOCITY? Run this once, in your project root:
// 
//          velocityx theme
// 
//    It writes velocity-preset.cjs + velocity-theme.css. Add the preset to your
//    tailwind.config, import the css, then set your whole brand in ONE place:
// 
//          :root {
//            --vx-brand: custom;
//            --vx-primary-600: 220 38 38;   <- your action colour
//            --vx-primary-700: 185 28 28;
//          }
// 
//    ⚠️  Raw RGB channels, space separated — NOT hex. `#DC2626` here silently
//        breaks every /opacity variant in the library (bg-primary-600/20 etc).
// 
//    Prefer not to install anything? Find-and-replace `primary-` with your own
//    scale name. Nothing else in this file depends on Velocity.
/**
 * PullQuote — Velocity Growth Layer · Readability Kit
 * ────────────────────────────────────────────────────────────────────────────
 * A large, styled pull-quote that gives the eye a resting point and re-states
 * the single most important line of a long-form page. Renders as a semantic
 * <figure> + <blockquote> with an optional <figcaption> citation.
 *
 * Dark-mode aware, no motion (reduced-motion friendly by construction).
 *
 * Usage:
 *   <PullQuote quote="Speed-to-ship is a feature." cite="Aidxn Design" />
 */
export interface Props {
  quote: string;
  cite?: string;
}

const { quote, cite } = Astro.props;
---
<figure class="not-prose my-8 sm:my-10">
  <div class="relative pl-6 sm:pl-8">
    <span
      aria-hidden="true"
      class="absolute -top-2 left-0 select-none font-serif text-5xl sm:text-6xl leading-none text-primary-500/30 dark:text-primary-300/30"
    >&ldquo;</span>
    <span
      aria-hidden="true"
      class="absolute inset-y-1 left-0 w-1 rounded-full bg-gradient-to-b from-primary-500 to-primary-400/30 dark:from-primary-300 dark:to-primary-400/30"
    ></span>
    <blockquote class="m-0">
      <p class="m-0 text-xl sm:text-2xl md:text-3xl font-semibold leading-snug tracking-tight text-secondary-900 dark:text-white text-balance">
        {quote}
      </p>
    </blockquote>
    {cite && (
      <figcaption class="mt-3 flex items-center gap-2 text-sm font-medium text-primary-700 dark:text-primary-300">
        <span class="inline-block h-px w-6 bg-primary-500/50 dark:bg-primary-300/50" aria-hidden="true"></span>
        <cite class="not-italic">{cite}</cite>
      </figcaption>
    )}
  </div>
</figure>

components/velocity/readability-kit/ReadingTime.astro

astro
---
//    VELOCITY COMPONENT · theming
// 
//    Colours here are Tailwind `primary-*` / `accent-*`, which resolve through
//    the Velocity preset. Out of the box they are a neutral indigo — NOT the
//    Aidxn brand — so this drops into your project without dragging our look in.
// 
//    ⚡ NOT USING VELOCITY? Run this once, in your project root:
// 
//          velocityx theme
// 
//    It writes velocity-preset.cjs + velocity-theme.css. Add the preset to your
//    tailwind.config, import the css, then set your whole brand in ONE place:
// 
//          :root {
//            --vx-brand: custom;
//            --vx-primary-600: 220 38 38;   <- your action colour
//            --vx-primary-700: 185 28 28;
//          }
// 
//    ⚠️  Raw RGB channels, space separated — NOT hex. `#DC2626` here silently
//        breaks every /opacity variant in the library (bg-primary-600/20 etc).
// 
//    Prefer not to install anything? Find-and-replace `primary-` with your own
//    scale name. Nothing else in this file depends on Velocity.
/**
 * ReadingTime — Velocity Growth Layer · Readability Kit
 * ────────────────────────────────────────────────────────────────────────────
 * A small "~N min read" pill with a clock icon. Pass `words` (word count) and
 * it estimates at `wpm` (default 225 — average adult reading speed), or pass
 * `minutes` directly to override the calc.
 *
 * Renders inline so it sits next to a date/author byline. The icon is
 * decorative; the visible text carries the meaning. Dark-mode aware, no motion.
 *
 * Usage:
 *   <ReadingTime words={1200} />
 *   <ReadingTime minutes={4} />
 */
export interface Props {
  words?: number;
  minutes?: number;
  wpm?: number;
}

const { words, minutes, wpm = 225 } = Astro.props;

const computed =
  typeof minutes === 'number'
    ? Math.max(1, Math.round(minutes))
    : Math.max(1, Math.round((words ?? 0) / wpm));
---
<span
  class="not-prose inline-flex items-center gap-1.5 rounded-full border border-primary-500/15 dark:border-primary-300/15 bg-primary-50/80 dark:bg-primary-500/10 px-2.5 py-1 text-xs font-medium text-primary-700 dark:text-primary-300"
>
  <svg class="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1.8" aria-hidden="true">
    <path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
  </svg>
  <span>~{computed} min read</span>
</span>

Theming

Uses the shared primary-* / accent-* scale — see Theming if colours look unstyled after install.