Typography

ReadMore

Text cut after N lines with a Read more / Show less button, left out when the text fits.

#typography#text#button#interactive

Demo

Preview

Quillo renders a page on the server, then resumes its components in the browser from the state the server sent, so a page shows at once and reacts as soon as it is touched. What someone typed and clicked before the script came is kept and done then. A component that runs by itself loads as soon as the page is idle; the rest wait until they are reached, so a page with a hundred of them costs no more than one. Pages with the same components share one bundle, whichever of them each showed.

A second paragraph, hidden until the button is pressed.

Quillo renders a page on the server, then resumes its components in the browser from the state the server sent, so a page shows at once and reacts as soon as it is touched. What someone typed and clicked before the script came is kept and done then. A component that runs by itself loads as soon as the page is idle; the rest wait until they are reached, so a page with a hundred of them costs no more than one. Pages with the same components share one bundle, whichever of them each showed.

Folded.

Short enough to fit, so there is no button.

Get it

It needs theme.qwe, as every component does, and line_clamp.qwe; or take the whole kit, quillo-ui.qwe. Each component brings its own styles to the pages that show it.

$ curl -O https://quillolang.org/components/ui/theme.qwe$ curl -O https://quillolang.org/components/ui/line_clamp.qwe$ curl -O https://quillolang.org/components/ui/read_more.qwe
main.qwe
import ui/theme.qwe use theme_cssuse ui/read_more.qweroute GET / = <html><head><style>{theme_css}</style></head><body class="ui-root">...</body></html>

Example

The code of the preview above.

demos/read_more.qwe
let read_more_demo_text = "Quillo renders a page on the server, then resumes its components in the browser from the state the server sent, so a page shows at once and reacts as soon as it is touched. What someone typed and clicked before the script came is kept and done then. A component that runs by itself loads as soon as the page is idle; the rest wait until they are reached, so a page with a hundred of them costs no more than one. Pages with the same components share one bundle, whichever of them each showed."component ReadMoreDemoLive() {    state open = false    <div.demo-stack>        <ReadMore lines={2} id="read-more-demo-live" more="See all" less="See fewer" on_toggle={(o) => { open = o }}>            {read_more_demo_text}        </ReadMore>        <p.demo-note>{open ? "Unfolded." : "Folded."}</p>    </div>}view ReadMoreDemo() =    <div.demo-stack.demo-narrow>        <ReadMore lines={3} id="read-more-demo-three">            <p>{read_more_demo_text}</p>            <p>A second paragraph, hidden until the button is pressed.</p>        </ReadMore>        <ReadMoreDemoLive/>        <ReadMore lines={3} id="read-more-demo-short">Short enough to fit, so there is no button.</ReadMore>    </div>

Source

ui/read_more.qwe
// ReadMore: text cut after a few lines, with a button that shows the rest and// folds it again. The button is left out when the text fits as it is.// A component; it needs line_clamp.qwe (it clamps with LineClamp).////   <ReadMore lines={3}><p>{post.body}</p></ReadMore>//   <ReadMore lines={2} more="See all" less="See fewer" open={true}>{long_text}</ReadMore>//// children: the text or HTML. lines: how many show when folded (3). more ("Read more")// and less ("Show less"): the button's labels. open: starts unfolded.// id: the text's id, which the button's aria-controls names ("ui-read-more").// on_toggle(open): inside one of your components, called when it is folded or unfolded.import ui/line_clamp.qwe use LineClampimport ui/theme.qwe use ui_clslet read_more_css = css`.ui-read-more { display: grid; gap: 6px; justify-items: start; font: 15px/1.5 var(--ui-font); color: var(--ui-fg); }.ui-read-more-btn { display: inline-flex; align-items: center; gap: 4px; margin: 0; padding: 0; border: 0; background: none;  border-radius: 3px; color: var(--ui-accent); font: 500 14px/1.4 var(--ui-font); cursor: pointer; }.ui-read-more-btn:hover { text-decoration: underline; text-underline-offset: 2px; }.ui-read-more-btn:focus-visible { outline: none; box-shadow: var(--ui-focus); }.ui-read-more-btn svg { width: 12px; height: 12px; transition: transform .15s; }.ui-read-more-open .ui-read-more-btn svg { transform: rotate(180deg); }@media (prefers-reduced-motion: reduce) { .ui-read-more-btn svg { transition: none; } }`component ReadMore(lines: int = 3, more: str = "Read more", less: str = "Show less", open: bool = false,                   id: str = "ui-read-more", on_toggle = null, class: str? = null, children) {    style global read_more_css    state shown = open    state needed = true    // false once the browser finds the text fits in its lines    <div class={ui_cls(["ui-read-more", shown && "ui-read-more-open", class])}         @mount={(el) => {             let t = el.querySelector(".ui-read-more-text")             if !shown && t != null && t.scrollHeight <= t.clientHeight + 1 { needed = false }         }}>        <LineClamp lines={shown ? 0 : lines} id={id} class="ui-read-more-text">{children}</LineClamp>        {needed && <button.ui-read-more-btn type="button" aria-expanded={shown ? "true" : "false"} aria-controls={id}                          @click={() => {                              shown = !shown                              on_toggle && on_toggle(shown)                          }}>            {shown ? less : more}            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"                 stroke-linejoin="round" aria-hidden="true"><path d="M3.5 6l4.5 4.5L12.5 6"/></svg>        </button>}    </div>}
Thread
56Useful? Like it, or add to its thread below.

Thread 0 posts

On ReadMore: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.