Forum Discussion

DShaw's avatar
DShaw
Community Member
6 hours ago

How Rise Canvas was created

I’ve had several requests to detail how I created Rise Canvas

Rather than trying to respond to everyone via DM, LinkedIn etc, I thought I’d detail everything here in one post.

I’m a web/app designer in addition to being an ID (and some other side hustles that pay the bills!) so this has been a bit of a background passion project initially started for my own work…

This will be a long read, so I’ll write it in several parts… This will be fairly non-technical as this isn’t really the place to discuss technical detail.. 

Part 1 coming next…

 

7 Replies

  • DShaw's avatar
    DShaw
    Community Member

    The Problem I was trying to solve

    Articulate Rise is a fantastic e-learning authoring tool, but its built-in block library has limits. When you need something beyond what’s in the standard library, a custom stats infographic, a branching scenario with a bit more functionality, the ability to mix and match interactions and infographics in the same block, re-size components and align them how you want, your options are limited. The introduction of the Rise code block, which lets you drop raw HTML, CSS and JavaScript directly into a lesson was a game changer for me. The potential is enormous.

    So this was the starting point. I knew I could find a better way to do what I needed in my courses. 

    Phase 1 — The Individual Component Builders

    The first decision was foundational: what format should these tools take?

    I landed on self-contained single HTML files. No frameworks, no build tools, no dependencies, no server required. Every builder is one file that opens in a browser and just works. This was a deliberate choice, the tools needed to be simple to distribute, simple to open, and simple to maintain.

    Each builder file contains three things inside a single HTML document:

    • The UI — a panel of controls on the left (text inputs, colour pickers, sliders, toggles)

    • A live preview — an iframe on the right that renders exactly what Rise will render

    • A buildHtml() function, this is the engine that reads the current settings and generates the finished HTML string.

    Building the first few

    The first builders were the most important, because they established the pattern everything else would follow. I started with the components that get I used most, accordion, tabs, flip card, etc and built each one by working through a consistent set of questions:

    • What content fields does this component need? (headings, body text, images, icons)

    • What style options are genuinely useful? (colours, font sizes, layout variants)

    • What does the output HTML need to do at runtime in Rise? (animations, interactions, accessibility)

    Every builder was written with all settings held in a JavaScript object called S (for State). Every UI control reads from S and writes back to it. When anything changes, a refresh() function regenerates the preview. This means the preview is always a true representation of what you’ll get.

  • DShaw's avatar
    DShaw
    Community Member

    Designing the design system

    One of the most important early decisions was establishing a shared design language across all 19 builders so they felt like one product rather than 19 separate experiments.

    That meant deciding on:

    • Colours: all stored as constants and used consistently

    • Typography: Poppins throughout, loaded from Google Fonts, with consistent size scales

    • UI components: the same input styles, label styles, card editor layouts, toggle switches and colour pickers appearing in every builder

    • Spacing: consistent padding, border-radius and gap values so the control panels feel like a system

    This paid off enormously later. Because every builder shares the same visual DNA, users can move between them without relearning anything.

    Accessibility requirement

    Every piece of output HTML had to work properly in Rise, which means it had to be accessible. From the start, every component was built with:

    • Correct ARIA roles and labels

    • Keyboard navigation (tab, enter, space, escape where relevant)

    • Appropriate colour contrast

    • Screen reader-friendly markup

    This was baked in from the beginning rather than added as an afterthought, which saved a lot of pain later.

  • DShaw's avatar
    DShaw
    Community Member

    Testing the output

    Testing the builders followed a consistent pattern for each new component:

    1. Build the component in the builder UI with a range of content

    2. Copy the HTML output

    3. Paste into a real Rise course as a code block

    4. Preview in Rise on both desktop and mobile viewpoints

    5. Check keyboard navigation works

    6. Check the output still looks right if Rise changes its surrounding styles

    This cycle repeated for every component and for every significant change. Bugs found at this stage were commonly, template literal escaping issues, CSS specificity conflicts with Rise’s own styles, or interaction timing problems with Rise’s (sometimes) lazy-loading behaviour (sorry articulate!)

  • DShaw's avatar
    DShaw
    Community Member

    Phase 2 — Rise Canvas

    Once I had 19 individual builders, a new problem emerged, they were still 19 separate things. Building a Rise lesson that used multiple custom components meant opening multiple builder files, copying HTML from each one, and pasting them separately into separate code blocks. It worked, but it wasn’t very nice and clunky to do.

    Rise Canvas was the answer, a tool that brings all 19 builders together onto a single infinite canvas, lets you arrange them visually, and exports them all in a single HTML blob for one Rise code block.

    The canvas architecture

    The canvas is itself a single HTML file (index.html) that contains:

    • A sidebar listing all 19 component types

    • A canvas surface where block cards are placed

    • A builder panel, a sliding iframe that loads whichever individual builder the user clicks, fully interactive

    • An export function that assembles all block HTML into a single output

    When a user selects a component from the sidebar, the relevant builder loads inside the panel iframe. They configure it exactly as they would in the standalone builder. When they click “Add to canvas”, the canvas calls buildHtml() on the builder iframe, pulls the generated HTML string out, and stores it in a block object with position, size and metadata.

    Each block on the canvas is rendered as a card, an iframe showing the live component preview, surrounded by a header bar with edit, duplicate and delete controls, plus resize handles at the corners and edges.

  • DShaw's avatar
    DShaw
    Community Member

    Drag, resize and layout

    Blocks are absolutely positioned on the canvas surface. Dragging works by tracking mouse delta from the drag start position, scaled by the current zoom level. Resizing works similarly, tracking which handle is being dragged (north, south, east, west, and diagonals) and adjusting width, height and position accordingly.

    A zoom control lets users scale the whole canvas in and out without affecting the actual pixel dimensions of the output components. This uses a CSS transform: scale() on the canvas surface.

    Design mode

    Build mode shows all the block controls, headers, resize handles, edit buttons. Design mode strips all of that away to give a clean view of just the components, making it much easier to assess how the layout actually looks. In design mode, clicking anywhere on a block starts a drag rather than requiring the header, since the header is hidden.

    Phase 3 — Improvements 

    Once the core was solid, I added features in layers, each one making the tool meaningfully better without breaking what was already there.

    Smart alignment guides

    When dragging a block, the tool compares the dragged block’s six reference points (left edge, right edge, horizontal centre, top edge, bottom edge, vertical centre) against the same six points on every other block. If any pair comes within 8 pixels of each other, a teal dashed guide line appears across the canvas at that position and the block snaps to exact alignment. The guides disappear the moment you release.

    Undo / Redo

    Every action that changes the canvas state, adding a block, moving it, resizing it, deleting it, duplicating it, stacking, clearing, pushes a clone of the entire blocks array onto a history stack, capped at 50 entries. Ctrl+Z walks backwards through the stack, Ctrl+Y (or Ctrl+Shift+Z) walks forward. The undo and redo toolbar buttons grey out automatically when there’s nothing to undo or redo.

    Auto-save

    Every time the history stack is pushed, a 2-second debounced timer starts. When it fires, the current blocks array is serialised to JSON and written to localStorage under the key riseCanvas_v1. On next load, the canvas silently reads that key and restores the previous session.  If someone’s uploading lots of images (which are stored as base64 strings inside the block HTML), the JSON can get large, browsers allow around 5–10MB for local storage, and if that limit is hit, I ensured the save fails silently.

    Image uploads

    Several components, Stats & KPI, Expanding Infographic, Flip Card, Carousel, support per-card background or inline images. The upload mechanism uses a hidden <input type="file"> element triggered programmatically. When a file is selected, the browser’s filereader API converts it to a base64 data URL which gets stored directly in the component state. This means the image is self-contained inside the exported HTML, no external hosting required. The trade-off is file size, which is why every upload field I nudged users toward squoosh.app for compression first. I have no link to squoosh I just think it’s brilliantly simple and effective. 

  • DShaw's avatar
    DShaw
    Community Member

    What Made It Work

    A few things made the difference between a collection of useful tools and a cohesive product:

    Establishing the pattern early. The decisions made on the first builder, stateful S object, refresh() function, buildHtml() output, consistent UI structure, meant every subsequent builder had a template to follow. Consistency was free once the pattern was set.

    Treating the output as seriously as the UI. The HTML that comes out of these builders lives inside a Rise lesson that real learners interact with. Every component had to meet the same standard as Rise’s own blocks, accessible, responsive, reliable.

    Iterating based on real use. Each new feature came from an actual friction point, the canvas was too cluttered without a design mode; losing work to a browser refresh was frustrating; manually aligning blocks was tedious. Features that solved real problems stayed. Features that added complexity without clear value didn’t make the cut.

    Keeping it self-contained. The zero-dependency, single-file approach meant the tools could be distributed, opened and used with zero setup. No npm install. No local server required (for basic use). No accounts. Just open and go.

  • DShaw's avatar
    DShaw
    Community Member

    The Stack

    For the curious, everything is vanilla HTML, CSS and JavaScript. No React, no Vue, no build pipeline, no bundler. The most “exotic” APIs in use are localStorage, FileReader, ResizeObserver, window.postMessage and CSS custom properties. The whole system, 20 files, 19 component builders plus the canvas, comes in under 210KB unzipped.