Style Live URL keyboards and dropdowns
Live URLs draw their own virtual keyboard and <select> menus so touch viewers can type and pick options. This page shows how to resize and restyle those controls, for example to give phone users bigger tap targets or to match your app's colors, and how to keep the styles in place after the page navigates.
- A Browserless API token from your account dashboard
- Node.js 18+ with
puppeteer-coreinstalled - Familiarity with creating a Live URL
- A touch device, or a desktop browser with touch emulation, to view the result
Where the styles go
Browserless injects the keyboard and replacement menus into the remote page, and the viewer sees them in the streamed frames like any other page content. Add your CSS to that remote page from your automation code. CSS in your own app, the iframe's parent page, or the Live URL viewer can't reach them.
Both controls require emulateComponents: true, which is the default. The keyboard appears only when the viewer has a touchscreen or coarse pointer and focuses a supported text field. Viewers with a mouse use their physical keyboard instead. Replacement dropdowns appear on desktop viewers too.
Runnable example: larger keyboard and styled dropdown
Save this as style-live-url.mjs and run it with BROWSERLESS_TOKEN=YOUR_API_TOKEN_HERE node style-live-url.mjs. To use a different region or a self-hosted instance, set BROWSERLESS_ENDPOINT.
The script loads a test form, adds CSS to the current page and every page after it, then prints a Live URL. Open that link on a touch device, or turn on touch emulation in your browser's developer tools before you open it. Tap the text field to bring up the keyboard and open the dropdown. Then follow Next page and check that the styles are still there.
import puppeteer from 'puppeteer-core';
const endpoint = new URL(
process.env.BROWSERLESS_ENDPOINT || 'wss://production-sfo.browserless.io/chromium',
);
endpoint.searchParams.set('token', process.env.BROWSERLESS_TOKEN);
const liveURLStyles = `
div.simple-keyboard.hg-theme-default {
--live-key-height: 52px;
--live-key-font: 18px;
--live-key-gap: 8px;
top: auto;
bottom: 0;
zoom: 1;
padding: 8px;
background: #0f172a;
color: #f8fafc;
}
div.simple-keyboard.hg-theme-default .hg-button {
height: var(--live-key-height);
font-size: var(--live-key-font);
background: #334155;
color: #f8fafc;
border-radius: 8px;
}
div.simple-keyboard.hg-theme-default .hg-button.hg-activeButton {
background: #2563eb;
}
div.simple-keyboard.hg-theme-default .hg-row:not(:last-child) {
margin-bottom: var(--live-key-gap);
}
div.simple-keyboard.hg-theme-default .hg-row .hg-button:not(:last-child) {
margin-right: var(--live-key-gap);
}
body div.cb-pvt-dropdown {
background-color: #0f172a !important;
border: 1px solid #64748b !important;
color: #f8fafc;
font-size: 18px !important;
border-radius: 8px;
}
body div.cb-pvt-dropdown .cb-pvt-option {
min-height: 44px;
padding: 10px 14px;
box-sizing: border-box;
}
body div.cb-pvt-dropdown .cb-pvt-option.cb-pvt-selected {
background-color: #2563eb;
color: #ffffff;
}
`;
function installLiveURLStyles(css) {
const sheet = new CSSStyleSheet();
sheet.replaceSync(css);
document.adoptedStyleSheets = [...document.adoptedStyleSheets, sheet];
}
const browser = await puppeteer.connect({ browserWSEndpoint: endpoint.href });
try {
const page = await browser.newPage();
// Covers reloads and full navigations, which start a fresh document.
await page.evaluateOnNewDocument(installLiveURLStyles, liveURLStyles);
// evaluateOnNewDocument skips the current document, so style it directly.
await page.evaluate(installLiveURLStyles, liveURLStyles);
const nextPage = `
<!doctype html><meta name="viewport" content="width=device-width, initial-scale=1">
<h1>Next page</h1><label>Message <input type="text"></label>
<label>Size <select><option>Small</option><option selected>Large</option></select></label>
`;
await page.goto('data:text/html,' + encodeURIComponent(`
<!doctype html><meta name="viewport" content="width=device-width, initial-scale=1">
<h1>Styled controls</h1><label>Message <input type="text"></label>
<label>Size <select><option>Small</option><option selected>Large</option></select></label>
<p><a id="next">Next page</a></p>
<script>
document.querySelector('#next').href = URL.createObjectURL(
new Blob([${JSON.stringify(nextPage)}], { type: 'text/html' }),
);
</script>
`));
const cdp = await page.createCDPSession();
const { error, liveURL, liveURLId } = await cdp.send('Browserless.liveURL', {
emulateComponents: true,
interactable: true,
resizable: true,
timeout: 120000,
});
if (error) throw new Error(error);
console.log('Open on a touch-capable viewer:', liveURL);
// Stay connected so the Live URL remains open while you test.
await new Promise((resolve) => setTimeout(resolve, 90000));
await cdp.send('Browserless.closeLiveURL', { liveURLId });
} finally {
await browser.close();
}
Expected output:
Open on a touch-capable viewer: https://production-sfo.browserless.io/live/index.html?i=...
The Live URL can't outlast the browser session, so make sure the session has at least two minutes left. See session duration limits.
Make the keyboard smaller
For a compact keyboard, change the three custom properties in liveURLStyles:
div.simple-keyboard.hg-theme-default {
--live-key-height: 32px;
--live-key-font: 14px;
--live-key-gap: 4px;
}
Key height sets the keyboard's overall height, font size sets the label size, and the gap sets the spacing between rows and keys. Keep top: auto; bottom: 0 from the full example. The default keyboard is pinned to a fixed top offset, so without these a shorter keyboard floats above the bottom edge. Keep zoom: 1 too, because the default keyboard is zoomed and the zoom would scale your pixel values. The keyboard always spans the full width of the remote viewport. Small keys are harder to tap, so test on the devices your users have.
Selectors and override rules
| Selector | What it styles |
|---|---|
div.simple-keyboard.hg-theme-default | Keyboard container, background, and overall positioning |
.simple-keyboard .hg-button | Keys, including height, labels, and colors |
.simple-keyboard .hg-row | Rows and their spacing |
.simple-keyboard .hg-button.hg-activeButton | Pressed key appearance |
div.cb-pvt-dropdown | Replacement dropdown panel |
.cb-pvt-dropdown .cb-pvt-option | Option sizing, spacing, and text |
.cb-pvt-dropdown .cb-pvt-option.cb-pvt-selected | Highlighted option, initially the selected value and then the hovered option |
Browserless adds its default styles through document.adoptedStyleSheets, and they can load after yours. That's why the example uses more specific selectors than the defaults: specificity wins no matter which stylesheet loads first.
The dropdown needs !important in two cases:
- Background and border. The defaults already mark these
!important, along with fixed positioning. - Font size and font family. Each time the dropdown opens, Browserless copies these from the original
<select>as inline styles, and inline styles beat any selector.
Don't override the dropdown's position, width, maximum height, scrolling, or display. Browserless sizes and places the menu based on the original <select> and the space left in the viewport, and overrides can push it off screen. To change option height, style .cb-pvt-option. Styling the original <option> elements has no effect on the replacement menu. The closed <select> is still your own element, so style it as usual.
Keep styles after navigation
page.addStyleTag() and a one-off page.evaluate() only style the current document, so the styles disappear on the next full navigation. The example pairs page.evaluate() for the current document with page.evaluateOnNewDocument() for every later one. Because the startup script runs before page scripts, your CSS is in place before Browserless creates its controls. Client-side SPA navigation keeps the stylesheet, unless the app overwrites document.adoptedStyleSheets.
With Playwright, swap the two install calls for these. installLiveURLStyles and liveURLStyles don't change:
await page.addInitScript(installLiveURLStyles, liveURLStyles);
await page.evaluate(installLiveURLStyles, liveURLStyles);
Startup scripts are per page, so new tabs don't inherit them. Install the styles on each page you automate. Startup scripts do run in child frames as they load or navigate. A frame that's already loaded needs its own frame.evaluate(installLiveURLStyles, liveURLStyles) call.
Limitations
- These class names are internal, not a versioned styling API. They can change, so recheck your styles after Browserless updates.
- CSS only changes appearance. It can't turn on touch input, change the keyboard layout, or make a view-only link interactive.
- The keyboard covers text, search, URL, telephone, password, and email inputs, plus textareas. It doesn't replace date or number pickers, and it doesn't appear for
contenteditableelements. - The replacement dropdown skips
<select multiple>and any select marked withno-proxy-select. Custom JavaScript dropdowns aren't replaced, so they keep their own styling. - The stylesheet changes the remote page for every viewer and for your automation. If your own components use these class names, the rules apply to them too.
- The stream scales with both the viewer and the remote viewport, so large controls can cover page content. Test at the device sizes and on the pages your users will see.
FAQ & Troubleshooting
Why don't my styles change the keyboard or dropdown?
Add the CSS to the remote page through your automation, not to your app or the iframe's parent page. If the CSS is on the remote page, use selectors at least as specific as the ones in the example. Browserless's defaults can load after your stylesheet and win on equal specificity.
Why did my styles disappear after the page navigated?
Full navigations and reloads create a new document. Install the styles with page.evaluateOnNewDocument() in Puppeteer or page.addInitScript() in Playwright, plus a page.evaluate() call for the current document.
Why doesn't the virtual keyboard appear?
The viewer needs a touchscreen or coarse pointer, and emulateComponents must be true. On desktop, turn on touch emulation in your browser's developer tools before you open the Live URL. The keyboard only appears for supported input types and textareas.
Why doesn't the dropdown font size change?
Browserless copies the font size and family from the original <select> as inline styles each time the dropdown opens. Add !important to your font-size or font-family rule.