Plain card
No accent. Body is Markdown, so links and code work.
Styleguide
The acceptance surface for the foundation phase. Tab switching and pane expansion are CSS-only, so they work with JavaScript disabled.
Surfaces, ink and brand swap with the theme. The plate and syntax tokens deliberately do not — code is dark in both themes, so the syntax palette is defined once.
Surfaces
--paper--chrome--panel--line--line-strongInk
--ink--dim--faintBrand
--red--red-text--red-soft--tealPlate (frozen)
--term-bg--term-chrome--term-tab--term-line--term-ink--term-dim--term-faint--term-red--term-tealSyntax (frozen)
--tok-sand--tok-amber--tok-violetOCR-A sets headings, labels, chips and buttons only — never running text, never below 11px. IBM Plex Sans carries the reading; IBM Plex Mono carries code.
OCR-A · display
IBM Plex Sans · body
Clicks the element found by selector. Waits for the element to be actionable before clicking — visible, stable, enabled and not obscured by another element. No explicit sleep is required.
IBM Plex Mono · code
Get Text h1 == Welcome
Literal shell only. The shell tabs preselect your own OS on load; picking one manually wins from then on. Prompts are excluded from selection, so a drag-copy yields runnable commands.
Pass and fail states, and a command too long to fit
All code. Tab bar with a file glyph, line-number gutter, status strip, copy button — and no sidebar, minimap or window controls. Lines 7–8 show the marked-range treatment.
*** Settings ***Library Browser*** Test Cases ***Sign In New Page https://example.com/login ${user} = Set Variable admin Fill Text id=user ${user} Fill Secret id=pass $PASSWORD Click text=Sign in Get Text h1 == Welcome
from Browser import Browserbrowser = Browser()def test_sign_in(): browser.new_page("https://example.com/login") browser.fill_text("id=user", "admin") browser.click("text=Sign in") browser.get_text("h1", "==", "Welcome")
Snippet, robot-repl grammar, no line numbers
New Page https://example.com/loginClick text=Sign inGet Text h1 == Welcome
Every content component, rendered from a Markdown file and shown beside that same file's source. The two cannot disagree — they are the same text.
Sections, grids and cards. These nest — a grid inside a section, cards inside a grid.
Rendered
Eyebrow label
A section owns its own padding and bottom rule, so a page is just a stack of
them. tone="panel" tints it, for alternating bands.
No accent. Body is Markdown, so links and code work.
The accent is painted rather than bordered, so it follows the bevel.
Use red for something actionable, green for something structural.
Markdown
::::page-section{label="Eyebrow label" title="A section heading"}A section owns its own padding and bottom rule, so a page is just a stack ofthem. `tone="panel"` tints it, for alternating bands.:::card-grid{min="14rem"}::card{title="Plain card"}No accent. Body is Markdown, so links and `code` work.::::card{title="Green accent" accent="green"}The accent is painted rather than bordered, so it follows the bevel.::::card{title="Red accent" accent="red"}Use red for something actionable, green for something structural.:::::::::
A plain Markdown list, laid out in columns with a marker.
Rendered
Markdown
::feature-grid- Conscientious assertions- Precise and fast browser window and tab control- Chainable selector strategies- Good shadow DOM support- Simple descriptors for mobile devices- Sending HTTP requests::
Buttons and a row of statistics. Both are inline components.
Rendered
Markdown
::btn-row:btn{to="/keywords" primary}[Primary action]:btn{to="/why"}[Secondary]:btn{to="https://github.com/MarketSquare/robotframework-browser"}[External, opens in a new tab]::::stat-row:stat{value="151"}[keywords]:stat{value="3"}[engines]:stat{value="0"}[sleeps]::
A fence is an editor. Several fences in a wrapper become one tabbed editor.
Rendered
A plain fence renders as an editor with no chrome:
Naming the file in the fence info string gives it a tab:
*** Settings ***Library Browser*** Test Cases ***Sign In Click text=Sign in
Several fences inside ::code-tabs become one editor with a tab each — the
code stays in the body, where it is readable and editable:
*** Test Cases ***Sign In New Page https://example.com/login Fill Text id=user admin Get Text h1 == Welcome
from Browser import Browserbrowser = Browser()def test_sign_in(): browser.new_page("https://example.com/login") browser.fill_text("id=user", "admin") browser.get_text("h1", "==", "Welcome")
Markdown
A plain fence renders as an editor with no chrome:```robot-replClick text=Sign inGet Text h1 == Welcome```Naming the file in the fence info string gives it a tab:```robot [login.robot]*** Settings ***Library Browser*** Test Cases ***Sign In Click text=Sign in```Several fences inside `::code-tabs` become one editor with a tab each — thecode stays in the body, where it is readable and editable:::code-tabs```robot [login.robot]*** Test Cases ***Sign In New Page https://example.com/login Fill Text id=user admin Get Text h1 == Welcome``````python [test_login.py]from Browser import Browserbrowser = Browser()def test_sign_in(): browser.new_page("https://example.com/login") browser.fill_text("id=user", "admin") browser.get_text("h1", "==", "Welcome")```::
Literal shell only. Structured, because prompts, output and result lines are different things.
Rendered
Unlike code, a terminal block stays structured. The prompt is not selectable, so copying yields runnable commands; output and result lines are styled differently because they are different. Supplying more than one shell gives tabs that preselect the reader's own OS.
Markdown
::terminal-block---sessions: - shell: bash steps: - command: pip install robotframework-browser output: [Successfully installed robotframework-browser] - command: rfbrowser init status: - { ok: true, text: chromium downloaded } - { ok: false, text: webkit failed — see rfbrowser.log } - shell: powershell steps: - command: py -m pip install robotframework-browser---::Unlike code, a terminal block stays structured. The prompt is not selectable,so copying yields runnable commands; output and result lines are styleddifferently because they *are* different. Supplying more than one shell givestabs that preselect the reader's own OS.
Two panes, either expandable to 75%. Generic — the panes hold anything, not only code.
Rendered
Click ⤢ on either pane to expand it; ⤡ restores parity. The narrow pane is never hidden — keeping both visible is the point.
Markdown
::split-panes---left: Beforeright: Afternotes: [No draggable divider, Works without JavaScript, Stacks below 640px]---#left```robot-replWait Until Element Is Visible id=userSleep 1sInput Text id=user admin${text}= Get Text h1Should Be Equal ${text} Welcome```#right```robot-replFill Text id=user adminGet Text h1 == Welcome```::Click ⤢ on either pane to expand it; ⤡ restores parity. The narrow pane isnever hidden — keeping both visible is the point.
Three kinds of aside, and a table authored in frontmatter.
Rendered
| Component | Body is | Use for |
|---|---|---|
::code-tabs | Fenced blocks | Several files as tabs |
::terminal-block | Frontmatter | Shell sessions |
::split-panes | Two named slots | Anything side by side |
Markdown
::doc-noteWorth knowing, but not urgent.::::doc-note{kind="warning"}This will bite you. Used for footguns and data loss.::::doc-note{kind="aside"}Context or a dissenting view — not instruction.::::doc-table---head: [Component, Body is, Use for]rows: - ['`::code-tabs`', 'Fenced blocks', 'Several files as tabs'] - ['`::terminal-block`', 'Frontmatter', 'Shell sessions'] - ['`::split-panes`', 'Two named slots', 'Anything side by side']---::
A headline that cycles through alternatives, decoding each one out of the last. Starts on a random line, pauses on hover, and does nothing at all under prefers-reduced-motion.
Rendered
Shown here at three seconds; the landing page holds each headline for five. Only the characters that differ from the previous headline scramble — which is why "Browser automation" sits still.
The third headline shows a deliberate line break: \n in the string breaks
the line there rather than wherever the column runs out. It works the same way
in ::page-hero's #title and in ::page-section{title="…"}, and it is
rendered with white-space: pre-line rather than a <br>, so the headline
stays one string — which is what lets the scramble measure and diff it.
Markdown
:::rotating-title---every: 3000titles: - "Browser automation that doesn't flake." - "Browser automation that doesn't suck." - "Browser automation\nbroken where you want it." - "Browser automation that sees shadow DOM."---:::Shown here at three seconds; the landing page holds each headline for five.Only the characters that differ from the previous headline scramble — which iswhy "Browser automation" sits still.The third headline shows a **deliberate line break**: `\n` in the string breaksthe line there rather than wherever the column runs out. It works the same wayin `::page-hero`'s `#title` and in `::page-section{title="…"}`, and it isrendered with `white-space: pre-line` rather than a `<br>`, so the headlinestays one string — which is what lets the scramble measure and diff it.
Two real files side by side, read from examples/ at build time, with an expandable divider and structural notes underneath.
Rendered
*** Settings ***Library Browser*** Test Cases ***My First Test [Documentation] Gets, types and asserts New Page https://example.cypress.io Click "type" Get Url *= /commands/actions Type Text .action-email fake@email.com Get Text .action-email == fake@email.com
describe('My First Test', () => { it('Gets, types and asserts', () => { cy.visit('https://example.cypress.io') cy.contains('type').click() cy.url().should('include', '/commands/actions') cy.get('.action-email').type('fake@email.com') cy.get('.action-email').should('have.value', 'fake@email.com') }) })
12 lines vs 9
The filenames and languages come from the paths — a tab cannot disagree with
the file it shows. notes is optional; the line count is added for you.
Markdown
::comparison{left="comparison/cypress/test.robot" right="comparison/cypress/test.cy.js"}---notes: - Each pane keeps its own language and its own horizontal scroll. - The line count is stated as a measurement; the notes are prose, so they are set in body type rather than the display face.---::The filenames and languages come from the paths — a tab cannot disagree withthe file it shows. `notes` is optional; the line count is added for you.
The top of a landing page — logo, headline, lede, and whatever you put under it.
Rendered
Two named slots: #title for the headline and the default slot for everything
else. On a narrow screen the logo moves above the text rather than shrinking
into the corner.
Markdown
:::page-hero{logo="/logo/browser.svg"}#titleA headline in the display face.#defaultThe default slot is the lede. Anything can follow it — buttons, figures, ora [link](/why) — and it all sits in the column beside the logo.::btn-row:btn{to="/docs/start/getting-started" primary}[Primary]:btn{to="/why"}[Secondary]:::::Two named slots: `#title` for the headline and the default slot for everythingelse. On a narrow screen the logo moves above the text rather than shrinkinginto the corner.
The core team and the full contributor wall, both generated from the library's all-contributors data.
Rendered
They review the pull requests, cut the releases and answer the hard questions. If you are wondering who decides, it is these two — in the open, on GitHub.
Browser exists because they built it. Stepping back from the core team does not undo that, so they stay listed here.
These two are data components rather than layout: they read
content/contributors.json, which scripts/build-contributors.ts generates
from the library's .all-contributorsrc. There is nothing to pass them.
::contributor-wall renders every contributor with a filter by kind of
contribution; ::contributor-wall{compact} renders faces only, which is what
the foot of the landing page uses. Neither is shown here — 206 avatars would
make this page about them rather than about the component.
Both are islands, so the data never reaches the browser.
Markdown
::core-team::These two are data components rather than layout: they read`content/contributors.json`, which `scripts/build-contributors.ts` generatesfrom the library's `.all-contributorsrc`. There is nothing to pass them.`::contributor-wall` renders every contributor with a filter by kind ofcontribution; `::contributor-wall{compact}` renders faces only, which is whatthe foot of the landing page uses. Neither is shown here — 206 avatars wouldmake this page about them rather than about the component.Both are islands, so the data never reaches the browser.
Click ⤢ on either pane to expand it to 75%; ⤡ restores parity. The other side is never hidden. Below 640px the panes stack and the control retires.
*** Settings ***Library Browser*** Test Cases ***Sign In New Page https://example.com/login Fill Text id=user admin Get Text h1 == Welcome
import { test, expect } from '@playwright/test';test('sign in', async ({ page }) => { await page.goto( 'https://example.com/login' ); await page.fill('#user', 'admin'); await page.click('text=Sign in'); await expect( page.locator('h1') ).toHaveText('Welcome');});