Forum Discussion
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…
11 Replies
- DShawCommunity Member
The problem I was trying to solve
Articulate Rise is a very good authoring tool, but its built-in block library has limits. When you need something beyond the standard set, a stats infographic, a branching scenario with a bit more logic, interactions and infographics mixed in the same block, components you can resize and align yourself, you quickly run out of road.
Then Articulate introduced the code block, which lets you drop raw HTML, CSS and JavaScript straight into a lesson. For me that changed what Rise could be. If I could generate the right HTML, I could put almost anything in a course. So that was the starting point.
Phase 1: the individual component builders
The first question was what format the tools themselves should take, and I settled on self-contained single HTML files. No frameworks, no build step. Every builder is one file that opens in a browser and works. I wanted them trivial to share with colleagues, and trivial for me to maintain a year later without reconstructing a toolchain first.
Each builder is one HTML document with three parts:
- A panel of controls on the left: text inputs, colour pickers, sliders, toggles
- A live preview on the right, an iframe rendering exactly what Rise will render
- A buildHtml() function that reads the current settings and generates the finished HTML string
Building the first few
The earliest builders mattered most because they set the pattern everything since has followed. I started with the components I reached for most in my own courses, accordion first, then tabs and flip cards, and worked through the same questions for each: what content fields does it need, which style options are genuinely useful (as opposed to configurable for its own sake), and what does the exported HTML have to do at runtime in Rise, animation, interaction, accessibility.
Every builder keeps all its settings in a single JavaScript object called S, for State. Every control reads from S and writes back to it, and any change triggers a refresh() that regenerates the preview. The result is that the preview is never a mock-up; it is the actual export, so what you see in the builder is what lands in Rise.
- DShawCommunity 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.
- DShawCommunity 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!)
- DShawCommunity 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.
- DShawCommunity 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.
- DShawCommunity 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.
- DShawCommunity 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.
- JudyNolletSuper Hero
DShaw: 👏 👏 👏
Thoughtful design + quality assurance = impressive!