Edit

Customization

<LearnHouseActivity> starts out looking exactly like LearnHouse. Everything on this page changes that, from a colour to replacing a block with your own component.

Theme

<LearnHouseActivity
  activityId="activity_…"
  theme={{
    mode: 'auto',              // 'light' | 'dark' | 'auto' (follows the OS)
    accent: '#7c3aed',         // links and accents
    background: '#ffffff',
    text: '#0f172a',
    mutedText: '#64748b',
    border: '#e2e8f0',
    fontFamily: 'Inter, sans-serif',
    headingFontFamily: 'Cal Sans, sans-serif',
    codeFontFamily: 'JetBrains Mono, monospace',
    fontSize: 17,              // numbers are pixels
    radius: 12,
    contentWidth: 720,         // or 'none' for full width
    padding: 24,
    cssVariables: { '--my-var': 'value' },
  }}
/>

Every value is optional; unset ones keep LearnHouse's look. A theme on LearnHouseProvider applies to every activity below it, and a theme on the component is merged on top.

The theme is applied as CSS custom properties (--lh-accent, --lh-font, …) on the root element, so you can also set them from your own stylesheet:

.lesson .lh-activity {
  --lh-accent: var(--brand-500);
  --lh-font: var(--font-sans);
}

In dark mode the text and background follow your theme, while blocks such as quizzes and callouts keep the light cards they have in LearnHouse.

Replace or wrap a block

components takes a component per block type. Each one receives the block's attrs, its rendered children (for blocks that contain text), and Default, which renders the built-in block. Wrap Default to decorate a block, or ignore it to replace the block entirely.

function Callout({ Default, ...props }) {
  return (
    <aside className="my-callout">
      <Default {...props} />
    </aside>
  )
}

function Heading({ attrs, children }) {
  const Tag = `h${Math.min(attrs.level + 1, 6)}` // shift headings down one level
  return <Tag className="lesson-heading">{children}</Tag>
}

<LearnHouseActivity activityId="activity_…" components={{ callout: Callout, heading: Heading }} />

Block names

BlockNameUseful attrs
Headingheadinglevel
Paragraph, lists, quoteparagraph, bulletList, orderedList, blockquote
CodecodeBlocklanguage
Tabletable, tableRow, tableCell, tableHeader
Calloutcallouttype (info, tip, success, warning, error)
QuizblockQuizquestions
Flipcard / gridflipcard, flipcardGridquestion, answer, color / columns
Scenarioscenariostitle, scenarios
Badge, buttonbadge, buttonemoji, color, link
MathblockMathEquationmath_equation
Image, video, audio, PDFblockImage, blockVideo, blockAudio, blockPDFblockObject
Code playgroundblockCode
Embed, web previewblockEmbed, blockWebPreview
User cardblockUser

Replace a mark

marks works the same way for inline formatting (bold, italic, code, link, …). A mark override replaces the markup entirely.

<LearnHouseActivity
  activityId="activity_…"
  marks={{
    link: ({ attrs, children }) => <MyLink href={attrs.href}>{children}</MyLink>,
  }}
/>

Hide blocks

<LearnHouseActivity activityId="activity_…" hideBlocks={['blockQuiz', 'blockCode']} />

Rewrite the content

transformContent receives the document before it renders. Filter blocks, inject your own, or annotate them:

<LearnHouseActivity
  activityId="activity_…"
  transformContent={(doc) => ({
    ...doc,
    content: doc.content.filter((block) => block.type !== 'horizontalRule'),
  })}
/>
<LearnHouseActivity
  activityId="activity_…"
  showTitle
  header={(activity) => <Breadcrumbs current={activity.name} />}
  footer={<NextLessonButton />}
  showPoweredBy
  siteUrl="https://learn.your-school.com"   // where "Made with LearnHouse" links to
/>

Loading and errors

<LearnHouseActivity
  activityId="activity_…"
  fallback={<LessonSkeleton />}
  errorFallback={(error, retry) => (
    <div role="alert">
      Couldn't load this lesson. <button onClick={retry}>Try again</button>
    </div>
  )}
/>

fallback shows while the activity loads and while a block's code downloads. errorFallback covers network errors, missing activities and activity types that can't render outside LearnHouse.

Links open in a new tab by default. Change that, or route them yourself:

<LearnHouseActivity
  activityId="activity_…"
  linkTarget="_self"
  onLinkClick={(href, event) => {
    if (href.startsWith('https://learn.your-school.com/')) {
      router.push(toLocalRoute(href))
      return false // cancel the browser's navigation
    }
  }}
/>

Your own CSS

The root element has the class lh-activity (plus dark in dark mode) and takes your className and style. Pass unstyled to drop the theme variables and style it all yourself.