New: the Halo × Pennylane integration by Halentra is live. Take a look
Halentra
Home/Blog/MDX components

Every block you can drop into an article

The MDX component set for Halentra's long-form content. Copy a block, fill in the content, keep the rhythm.

01 · Prose + link

Body copy

Default paragraph, bold and link styling, applied automatically to every MDX document through the shared Prose wrapper.

Body copy runs on a comfortable line height so long explanations stay easy to follow. Inline emphasis stays semantic and quiet, and a link looks like this one, styled through the shared text-link component rather than one-off inline styles.

usage.mdx
Body copy runs as plain Markdown. Use **bold** for
semantic emphasis and [a link](/demo) for anything clickable.
02 · Callouts

Callout

Three tones are wired up today: a soft mint box for general notes, a dark panel for anything readers must not skim past, and a light surface box for questions that deserve a pause.

Note

Rates are set per user, per month. Devices sit on their own line so growth stays visible.

Heads up

Never write “unlimited” into a contract. It is not a commercial term, it is a bet you lose on the worst possible week.

Question

Is this block the right container for a question? Use it when you want the reader to pause and actually answer before moving on.

usage.mdx
<Callout label="Note" tone="info">
  Rates are set per user, per month.
</Callout>

<Callout label="Heads up" tone="danger">
  Never write "unlimited" into a contract.
</Callout>

<Callout label="Question" tone="question">
  Is this block the right container for a question?
</Callout>
PropType
label
required
string
tone
optional
"info" | "danger" | "question"default "info"
children
required
ReactNode
03 · CTA

Ready to see it on your own data?

30 minutes, no slides.

usage.mdx
<Cta
  title="Ready to see it on your own data?"
  subtitle="30 minutes, no slides."
  primaryLabel="Book a demo"
  primaryHref="/demo"
  secondaryLabel="See pricing"
  secondaryHref="/solutions"
/>
PropType
title
required
string
subtitle
optional
string
primaryLabel
required
string
primaryHref
required
string
secondaryLabel
optional
string
secondaryHref
optional
string

The secondary button only renders when both secondaryLabel and secondaryHref are set.

04 · Image

Inline image

Renders full width by default, opens a full-screen lightbox on click, and preloads on hover so the transition feels instant.

Caption stays quiet and one line where possible.
usage.mdx
<MediaImage
  src="/assets/hudu/hero.webp"
  alt="Example product screenshot"
  maxWidth={360}
/>
PropType
src
required
string
alt
optional
string
maxWidth
optional
number

Caps the rendered width in px. Full width when unset.

05 · Quote

Pull quote

A discount is a price change. An exception is a scope change. Confusing the two is how margins disappear quietly.

- Marc Aubert, Halentra
usage.mdx
<PullQuote
  text="A discount is a price change."
  name="Marc Aubert"
  role="Halentra"
/>
PropType
text
required
string
name
required
string
role
required
string
06 · Testimonial

Customer quote

We stopped rebuilding quotes in a spreadsheet. Three tiers went into the PSA and renewals took half the time they used to.

Sarah Lindqvist
Operations lead, Northbound IT
usage.mdx
<Quote
  text="We stopped rebuilding quotes in a spreadsheet."
  name="Sarah Lindqvist"
  role="Operations lead, Northbound IT"
/>
PropType
text
required
string
name
required
string
role
required
string
07 · Table

Data table

TierCoverageWhat it includes
EssentialBusiness hoursPatching, monitoring, service desk.
StandardExtendedProject hours, quarterly reviews, a named engineer.
Critical24/7Out-of-hours response and a tested recovery plan.
usage.mdx
<DataTable
  columns={["Tier", "Coverage", "What it includes"]}
  rows={[
    ["Essential", "Business hours", "Patching, monitoring, service desk."],
  ]}
/>
PropType
columns
required
string[]
widths
optional
string[]

CSS grid track sizes, one per column. Defaults to an equal 1fr per column.

rows
required
ReactNode[][]
08 · FAQ

Frequently asked

Yes. Every block below is a standalone MDX component, so an article can move from prose to a callout to a table without any special wiring.

Use the closest available block, or ask engineering to add it. Every block the design calls for is rendered and documented on this page.

usage.mdx
<Faq
  items={[
    { q: "Can I mix components?", a: "Yes, freely." },
  ]}
/>
PropType
items
required
{ q: string; a: string }[]
09 · Key numbers

Key numbers

20
components live today
0
still on the wishlist
1
place they're all documented
usage.mdx
<KeyNumbers
  items={[
    { value: "12", label: "components live today" },
  ]}
/>
PropType
items
required
{ value: string; unit?: string; label: string }[]
10 · Timeline

Timeline

Step 1
Write the draft

Plain prose first, blocks added only where they earn their place.

Step 2
Drop in the blocks

Callouts for asides, a table for anything tabular, a quote where it's earned.

Step 3
Publish

The Prose wrapper keeps typography consistent across every collection.

usage.mdx
<Timeline
  steps={[
    { period: "Step 1", title: "Write the draft", desc: "Plain prose first." },
  ]}
/>
PropType
steps
required
{ period: string; title: string; desc: string }[]
11 · Pull highlight

Pull highlight

Clients forgive increases. They do not forgive surprises.

usage.mdx
<StatementBanner
  text="Clients forgive increases. They do not forgive surprises."
  accent="surprises"
/>
PropType
text
required
string
accent
required
string

Must be an exact substring of text. That substring is what gets the accent colour.

13 · Pros and cons

Pros and cons

A two-column card: a green checkmarked list of what works well next to a plain list of what to watch out for.

Works well
  • Quotes leave the office in minutes.
  • Renewals stop being a negotiation from scratch.
Watch out
  • Legacy contracts need a migration window.
  • Sales needs one training session, not a memo.
usage.mdx
<ProsCons
  works={{
    label: "Works well",
    items: ["Quotes leave the office in minutes."],
  }}
  watch={{
    label: "Watch out",
    items: ["Legacy contracts need a migration window."],
  }}
/>
PropType
works
required
{ label: string; items: string[] }
watch
required
{ label: string; items: string[] }
14 · Download

Download

A file card with an icon, title, and format/size subtitle, next to a pill button that downloads the attached document.

Pricing model templateXLSX · 82 KB
Download
usage.mdx
<Download
  label="Pricing model template"
  url="/api/media/files/cms-body/pricing-model-template.xlsx"
  fileName="pricing-model-template.xlsx"
  size="83968"
/>
PropType
label
required
string
url
required
string
fileName
required
string
size
required
string

Stringified byte count - the subtitle formats it into a human-readable size.

15 · Tabs

Tabs

A row of labeled tabs; clicking a label swaps the single paragraph body shown below it.

A short summary of what this plan covers, in one paragraph.

usage.mdx
<Tabs
  tabs={[
    { label: "Overview", body: "A short summary tab body goes here." },
    { label: "Pricing", body: "A second tab, swapped in on click." },
    { label: "Support", body: "A third tab, same paragraph treatment." },
  ]}
/>
PropType
tabs
required
{ label: string; body: string }[]
17 · Before and after

Before and after

A draggable image-comparison slider - drag the handle, or focus it and use the arrow keys, to reveal more of the after image.

BEFOREAFTER
usage.mdx
<BeforeAfter
  before="/assets/hudu/work-screen-capture.webp"
  after="/assets/hudu/work-hudini-ai.webp"
  beforeLabel="BEFORE"
  afterLabel="AFTER"
/>
PropType
before
required
string

Image URL shown at split 0.

after
required
string

Image URL revealed as the split grows.

beforeLabel
optional
stringdefault "BEFORE"
afterLabel
optional
stringdefault "AFTER"
18 · Ask an expert

Ask an expert

A compact dark banner pairing a circular avatar photo with a title/subtitle line and a bright pill CTA - drop it after a pricing or comparison section to route the reader to a named contact.

Not sure where your contracts stand?
Talk to us
usage.mdx
<AskExpert
  photo="/assets/testimonials/amerix-livio.png"
  title="Not sure where your contracts stand?"
  body="Send us your current price list. We will tell you what to keep."
  ctaLabel="Talk to us"
  ctaUrl="/contact"
/>
PropType
photo
optional
string

Circular avatar photo URL. Renders a bare placeholder circle when unset.

title
required
string
body
required
string
ctaLabel
required
string
ctaUrl
required
string
23 · Feature comparison

Feature comparison

A two-competitor table with category group headers, five per-cell content types (checks, an "Advantage" pill, crosses, plain text, and a linked price), optional per-row info tooltips, and a footer CTA bar.

Feature
Hudu
Distributed by Halentra
IT Glue
Alternative
Documentation
IPAMIP address management, built in.
Advantage
Unlimited assets
Checklists & runbooks
Advantage
Basic checklists
Photos & diagrams
Advanced
API access
Self-hosted optionRun it on your own infrastructure instead of the vendor's cloud.
Advantage
Demo only
Automation
Radar add-on, €9 / company / month
AI search
Advantage
Pricing and value
Included users
Unlimited users
5 users on standard plans
Starting priceBilled annually; monthly billing is available at a small premium.
€44 / user / month
Contract length
Monthly, no lock-in
Annual commitment
Based on public pricing pages, checked June 2026. Spotted something out of date?
Plan a migration
usage.mdx
<FeatureComparison
  columns={[
    { name: "Hudu", subtitle: "Distributed by Halentra" },
    { name: "IT Glue", subtitle: "Alternative" },
  ]}
  groups={[
    {
      label: "Documentation",
      rows: [
        {
          feature: "IPAM",
          tooltip: "IP address management, built in.",
          cells: [{ kind: "check" }, { kind: "cross" }],
        },
      ],
    },
  ]}
  footerNote="Based on public pricing pages, checked June 2026."
  footerLinkLabel="Spotted something out of date?"
  footerLinkUrl="/contact"
  ctaLabel="Plan a migration"
  ctaUrl="/contact"
/>
PropType
columns
required
[{ name: string; subtitle: string; logo?: string }, { name: string; subtitle: string; logo?: string }]

Exactly two entries. The first is the highlighted column - Halentra's own pick.

groups
required
{ label: string; rows: { feature: string; tooltip?: string; cells: [Cell, Cell] }[] }[]

Cell is one of { kind: "check" }, { kind: "check-advantage" }, { kind: "cross" }, { kind: "text"; text }, or { kind: "link"; text; href }.

footerNote
required
string
footerLinkLabel
optional
string
footerLinkUrl
optional
string

The inline footer link only renders when both footerLinkLabel and footerLinkUrl are set.

ctaLabel
required
string
ctaUrl
required
string
24 · Steps

Steps

A numbered walkthrough - each step gets a title and a description, with a screenshot underneath for the ones that need one.

1
HaloPSA → Pennylane

Invoices raised in HaloPSA sync to Pennylane automatically, no manual re-entry.

2
Match the payment

Pennylane reconciles the incoming payment against the synced invoice.

3
Close the books

Once matched, the invoice is marked paid on both sides - nothing left to reconcile by hand.

usage.mdx
<Steps
  steps={[
    {
      title: "HaloPSA → Pennylane",
      text: "Invoices raised in HaloPSA sync to Pennylane automatically.",
      image: "/assets/hudu/work-api.webp",
      alt: "Invoice sync screenshot",
      maxWidth: 480,
    },
    {
      title: "Close the books",
      text: "Once matched, the invoice is marked paid on both sides.",
    },
  ]}
/>
PropType
steps
required
{ title: string; text: string; image?: string; alt?: string; maxWidth?: number }[]

The badge number is the item's position in the array - image (and maxWidth, its cap in px) is optional per step.

25 · Image beside text

Image beside text

A fixed-width screenshot with a heading and one paragraph of context, mirrored left or right down a long article. Stacks image-over-text on mobile.

What the handoff looks like

Invoices raised in HaloPSA sync to Pennylane automatically, no manual re-entry. The screenshot sits beside the paragraph, and flipping `side` mirrors it into the other column.

usage.mdx
<ImageText
  src="/assets/hudu/work-api.webp"
  alt="Invoice sync screenshot"
  side="left"
  title="What the handoff looks like"
  text="Invoices raised in HaloPSA sync to Pennylane automatically."
/>
PropType
src
required
string
alt
optional
string
side
optional
"left" | "right"default "left"

Which column the image sits in; stacks image-over-text on mobile either way.

title
required
string
text
required
string
26 · Video embed

Video embed

A poster image with a play affordance over it; clicking swaps in the YouTube iframe, so page weight stays down until the reader asks for the video.

Click to play, no autoplay on load.
usage.mdx
<Video
  youtubeUrl="https://www.youtube.com/watch?v=AuulayTFuRU"
  caption="Click to play - no autoplay on load."
/>
PropType
youtubeUrl
required
string

A full YouTube URL (watch/youtu.be/shorts/embed/live), not a bare video id.

caption
optional
string

Rendered under the player when set, same optional treatment as other media captions.