Reference for building tool val UIs with Pico CSS v2. Read this when implementing the UI layer.
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="color-scheme" content="light dark"> <title>Tool Name</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"> <style> :root { --pico-font-size: 16px; } .container { max-width: 900px; } </style> </head> <body> <main class="container">...</main> </body> </html>
Key rules:
<meta name="color-scheme" content="light dark">.Use <hgroup> โ Pico automatically mutes the subtitle:
<hgroup> <h1>๐ Tool Name</h1> <p>What it does. Link to <a href="#">underlying library</a> if wrapping one.</p> </hgroup>
This is the one pattern we always write custom CSS for. Use ARIA roles:
<div role="tablist"> <button role="tab" aria-selected="true" aria-controls="tab-a">Tab A</button> <button role="tab" aria-selected="false" aria-controls="tab-b">Tab B</button> </div> <div role="tabpanel" id="tab-a" class="active">...</div> <div role="tabpanel" id="tab-b">...</div>
[role="tablist"] { border-bottom: 1px solid var(--pico-muted-border-color); margin-bottom: 1.5rem; }
[role="tab"] {
background: none; border: none;
border-bottom: 3px solid transparent; border-radius: 0;
padding: 0.75rem 1.5rem; margin: 0;
color: var(--pico-muted-color); font-weight: 500; cursor: pointer;
}
[role="tab"][aria-selected="true"] { color: var(--pico-primary); border-bottom-color: var(--pico-primary); }
[role="tab"]:hover { color: var(--pico-primary); }
[role="tabpanel"] { display: none; }
[role="tabpanel"].active { display: block; }
document.querySelectorAll('[role="tab"]').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('[role="tab"]').forEach(t => t.setAttribute('aria-selected', 'false'));
document.querySelectorAll('[role="tabpanel"]').forEach(p => p.classList.remove('active'));
tab.setAttribute('aria-selected', 'true');
document.getElementById(tab.getAttribute('aria-controls')).classList.add('active');
});
});
URL input with action button โ use Pico's fieldset group:
<fieldset role="group"> <input type="url" id="urlInput" placeholder="https://example.com" autofocus> <button id="fetchBtn" aria-busy="false">Extract</button> </fieldset>
Loading state โ Pico's aria-busy adds a spinner to buttons:
function setLoading(btn, loading) {
btn.setAttribute('aria-busy', loading);
btn.disabled = loading;
}
Enter key to submit:
urlInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') fetchBtn.click(); });
Code/markdown output with copy button (custom โ no Pico equivalent):
<div class="output-wrap"> <button class="copy-btn" data-tooltip="Copy to clipboard">⎘ Copy</button> <pre class="result"><code id="output"></code></pre> </div>
.output-wrap { position: relative; }
.copy-btn {
position: absolute; top: 0.5rem; right: 0.5rem;
background: var(--pico-secondary-background); border: 1px solid var(--pico-muted-border-color);
border-radius: 4px; padding: 0.3rem 0.6rem; font-size: 0.8rem; z-index: 1; cursor: pointer;
}
pre.result {
max-height: 70vh; overflow: auto; white-space: pre-wrap; word-break: break-word;
padding: 1rem; padding-top: 2.5rem; margin: 0; font-size: 0.85rem;
}
Metadata bar (custom โ no Pico equivalent):
.meta { display: flex; flex-wrap: wrap; gap: 0.5rem 1.5rem; font-size: 0.8rem; color: var(--pico-muted-color); }
Before writing custom CSS, check if Pico already handles it:
<article> with <header> and <footer><details> + <summary> (Pico styles it natively)aria-busy="true" on any element.secondary, .contrast, .outline variantsrole="group" wrapper, aria-current="true" for activedata-tooltip="text" on any element, pure CSSaria-invalid="true|false" on inputs, <small> helper text inherits color<input type="checkbox" role="switch"><details class="dropdown"> + <summary> + <ul><nav> with <ul> auto-distributes horizontally<dialog> + <article>, .modal-is-open on <html><progress> for determinate, <progress /> for spinner.grid for auto-layout equal columns<div class="overflow-auto"> for mobile<small>, <mark>, <kbd>, <del>, blockquotes all styled<footer> <a href="https://www.val.town/u/kamalnrf">kamalnrf</a> ยท <a href="https://github.com/...">source</a> </footer>
<details> CSS โ Pico already styles it as an accordion<fieldset role="group"> โ more semantic than <div role="group"> for form inputs<meta name="color-scheme"> โ needed for scrollbars and native controls to respect dark modedisabled nativelydata-tooltip โ free styled tooltips, better than title attributesvar(--pico-del-color) for error text<main>, not <article>.overflow-auto โ tables without this wrapper break on mobile<input type="checkbox" role="switch"><details class="dropdown"> instead<progress> โ <progress /> gives an indeterminate spinner for free<nav> with <ul> auto-distributes horizontally