Blog / Development

Interactive illustrations in React and Next.js

Embed snippets are written for plain HTML. React apps render on the client, change routes without reloading and may render on the server first. Here is how to use an interactive illustration embed cleanly in React and Next.js.

By Abdullah NomanOctober 16, 20263 min read

Code review diff, hairline isometric illustration

For still and animated illustrations in React, components are the natural fit, and we cover them in SVG illustrations in React and Next.js. Interactive figures from an embed script need a slightly different setup: the script owns what is drawn inside an element, and React owns the element. The trick is to keep those two jobs apart.

1. Load the script once

Next.js

Use next/script in your root layout. The Next.js docs list four strategies; for decorative figures, afterInteractive (the default) or lazyOnload both work. Avoid beforeInteractive, which is meant for critical scripts. In the Pages Router, put the same <Script> in pages/_app.tsx.

// app/layout.tsx
import Script from 'next/script'

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>
        {children}
        <Script src="https://liniqo.com/live/embed.js" strategy="lazyOnload" />
      </body>
    </html>
  )
}

Vite, Create React App and others

Add the script tag to index.html with async, or inject it once from your app entry:

if (!document.querySelector('script[src*="liniqo.com/live/embed.js"]')) {
  const s = document.createElement('script')
  s.src = 'https://liniqo.com/live/embed.js'
  s.async = true
  document.body.appendChild(s)
}

2. A small wrapper component

React renders an empty div with the right attributes; the script fills it. The wrapper gives it a 5:4 box so nothing shifts while it loads:

// components/LiveFigure.tsx
type Props = {
  name: string
  accent?: string
  line?: string
  bg?: string
  theme?: 'light' | 'dark'
  intensity?: number
  label?: string
  className?: string
}

export function LiveFigure({ name, accent, line, bg, theme, intensity, label, className }: Props) {
  return (
    <div className={className} style={{ width: '100%', aspectRatio: '5 / 4' }}>
      <div
        key={[name, accent, line, bg, theme, intensity].join('|')}
        data-liniqo={name}
        data-accent={accent}
        data-line={line}
        data-bg={bg}
        data-theme={theme}
        data-intensity={intensity}
        {...(label ? { role: 'img', 'aria-label': label } : { 'aria-hidden': true })}
      />
    </div>
  )
}

Use it like any component:

<LiveFigure name="ci-pipeline" accent="#7c3aed" bg="#ffffff" />

3. Client routing just works

The Liniqo Live script watches the page for new elements. When React mounts a data-liniqo element after a route change, in a modal or in a list, the script picks it up and loads that figure when it nears the screen. You do not need to call anything after navigation.

4. Changing colours or theme

A figure reads its attributes when it is drawn. To apply new colours, React needs to give it a fresh element, which is what the key in the wrapper does: when any option changes, React replaces the inner div, and the script draws a new figure with the new values. Connect theme to your theme state and the figure follows your dark mode toggle:

const { resolvedTheme } = useTheme()   // e.g. next-themes
<LiveFigure name="analytics-bars" theme={resolvedTheme === 'dark' ? 'dark' : 'light'}
            bg={resolvedTheme === 'dark' ? '#0e1013' : '#ffffff'} />

Without data-theme, a figure also goes dark by itself when an ancestor has the class dark or data-theme="dark" at the moment it is drawn, which matches Tailwind's class-based dark mode.

Live figure: move your pointer across it, or drag a finger on a phone. CI pipeline is one of 23 free interactive figures.

5. Server rendering and hydration

The wrapper renders the same empty element on the server and the client, so there is nothing to mismatch. Two rules keep it that way:

  • Do not render children inside the data-liniqo element. The script replaces its contents.
  • Load the script after hydration, which afterInteractive and lazyOnload do. A script that draws before React hydrates can produce hydration warnings.

6. TypeScript and lint notes

  • data-* attributes are valid on any JSX element; no type declarations are needed.
  • Store any licence token for Pro figures (the data-key in the copied embed code) in configuration rather than scattering it through components.

When to use components instead

If you want still or draw-on illustrations that live in your repo as code, use Liniqo's React components through the shadcn registry. Use the live embed for figures that respond to the pointer. Browse the 500 interactive figures in the library; 23 are free, and the rest come with Pro and Lifetime.

Questions

How do I add an external script in Next.js?

Use the Script component from next/script. The default strategy, afterInteractive, loads it after hydration; lazyOnload waits for browser idle time, which suits decorative embeds.

Do embedded figures work with React Router or Next.js client navigation?

Yes. The Liniqo Live script watches for new data-liniqo elements and mounts them when they appear, including after client-side route changes.

How do I change an embedded figure's colours from React state?

Give the element a key built from its options. When an option changes, React replaces the element and the script draws a new figure with the new attributes.

Will the embed cause hydration errors?

Not if the element renders without children and the script loads after hydration, as next/script's afterInteractive and lazyOnload strategies do.

Keep reading