Readability Kit
Free · MITFour 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
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
---
// 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
---
// 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
---
// 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"
>“</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
---
// 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.