The first time I opened a web page inside Emacs I thought it was a trick, a hack, a toy... studying it in depth I saw I was wrong. EWW is a browser written 100% in Emacs Lisp, with no external engine behind it (no WebKit, Blink or Gecko). It is absurdly lightweight, intelligently designed and, almost without meaning to, it hands you a platform to do scraping or automate web tasks in a few lines. A tool with enormous potential that many people don't know about, even within the Emacs community itself.And the best part is that you already have it installed. It ships with Emacs by default. To launch it just run M-x eww and type a URL or a search term (it will open DuckDuckGo).Is it a replacement for Chrome or Firefox? No, and it doesn't try to be. It plays in a different league.Two pieces: EWW and SHRWhat we call "the Emacs browser" is really two pieces working together.EWW (eww.el) is the browser layer: URLs, history, bookmarks, forms, cookies, downloads and sessions.SHR, or Simple HTML Renderer (shr.el), is the engine that turns HTML into text inside a buffer. And EWW is not the only one using it: Gnus for mail, elfeed for feeds, and quite a few other packages share it too.Here's the key: SHR doesn't draw a page, it translates it. It takes the HTML and paints it as Emacs text, with its faces and its properties. What does it understand along the way? Quite a bit more than you'd imagine:Rich text: b, i, em, strong, u, s, code, tt, mark, ins, del, sup, sub, abbr, bdo/bdi.Structure: h1..h6, p, div, blockquote, pre, hr, ul/ol/li, dl/dt/dd.Links and tables.Images: it understands data: URIs (base64), srcset (it picks the resolution), cid: (mail), scaling with shr-max-image-proportion, animation and zoom. And if a src is broken, it falls back to its alt text, as it should.MathML: it keeps the TeX annotation, it does not render the formula.Forms are a curious case: SHR doesn't add them, EWW layers them on top via shr-external-rendering-functions. Thanks to that you can submit GET and POST forms, and even multipart/form-data to upload files.Two things are missing from the list: JavaScript and CSS.What EWW doesn't do (and why that's fine)EWW is not meant to run modern web applications. Its limitations aren't an oversight, they are the reason it's so fast and so lightweight. But you'd better be clear about them before you get frustrated.No JavaScript. This rules out, in one stroke, any SPA (React, Vue, Angular), infinite scroll, content that arrives via fetch or XHR, and most of today's web. If a page needs JS to paint itself, in EWW you'll see little or nothing.No CSS. The code itself confesses it in its header: "It does not do CSS, JavaScript or anything advanced". In practice: sheets and are ignored completely. Only the inline style attribute is read, and only if it contains color, display (specifically none) or border-collapse. Everything else (font-size, margin, padding, float, flex, grid, text-align...) is thrown in the bin.Colors require (display-color-cells) >= 88. And the contrast system is surprisingly serious: it converts to CIE Lab and uses CIE DE2000 distance to make sure the text is readable.There are no class or id selectors, no cascade, no specificity. Nothing.The parser is not HTML5-conformant. It uses libxml2, which is tolerant but does not follow the HTML5 parsing algorithm to the letter. Manual patches are applied to plug the holes.EWW is not for SPAs, online banking, JS dashboards, dynamic forms, anything that throws a "enable JavaScript to continue" at you, embedded video or audio, or layouts that are only legible thanks to CSS. It's not its turf, and forcing it is a waste of time.Scraping out of the boxEWW leans on libxml-parse-html-region, which gives you back the DOM as an S-expression. And Emacs includes dom.el to walk it. That makes scraping trivial, and you don't even need to open EWW.Look at this script. It extracts the headlines from the Hacker News front page:(require 'dom)(require 'url)(require 'cl-lib)(defun demo-scrape (url) "Download URL and return the Hacker News headlines as a list of conses.Each element is (TITLE . HREF)." (with-current-buffer (url-retrieve-synchronously url t t 30) (goto-char (point-min)) ;; Skip HTTP headers until the first blank line. (re-search-forward "\r?\n\r?\n" nil t) (let* ((dom (libxml-parse-html-region (point) (point-max))) ;; On HN each headline is .... (titles (dom-by-class dom "titleline"))) (mapcar (lambda (node) (let ((a (dom-child-by-tag node 'a))) (cons (string-trim (dom-texts a)) ; link text (dom-attr a 'href)))) ; destination titles))))(defun demo-scrape-hn () "Download the Hacker News front page and show the headlines in a buffer." (interactive) (let ((items (demo-scrape "https://news.ycombinator.com/"))) (with-output-to-temp-buffer "*HN headlines*" (princ (format "Headlines found: %d\n\n" (length items))) (cl-loop for (title . href) in items for i from 1 do (princ (format "%2d. %s\n %s\n\n" i title href))))));; Evaluating the buffer (M-x eval-buffer) runs it directly:(demo-scrape-hn)Evaluate it with M-x eval-buffer and a temporary buffer will pop up with the list of headlines and their links. No external libraries, nothing to install.This is possible because dom.el gives you a handful of functions that do the heavy lifting: dom-by-tag, dom-by-class, dom-by-id, dom-child-by-tag, dom-attr, dom-text and dom-texts. And if instead of the raw HTML you want the already-rendered text (for example, to index the "readable" version of a page), you can run the DOM through shr-insert-document in a temporary buffer and keep buffer-string. This is, literally, the foundation on which elfeed or mu4e are built.Designing "for EWW" is designing wellLet me switch perspective. So far we've talked about EWW as a reader. But what if you're the one publishing? What if you want your site to look flawless in there?The good news is that designing for EWW isn't learning some weird dialect. It's going back to the principles of semantic HTML, the ones that should never have been abandoned. EWW renders HTML as a structured document, not as a canvas painted with CSS. Follow these ideas and, as a bonus, your site will be more accessible everywhere.DOM order is on-screen orderSHR walks the tree in document order and inserts the text just as it finds it. There is no CSS reordering: float, flex, grid, order and position don't exist. Whatever you put first in the HTML appears first.So place the main content as early as possible in the DOM, or at least right after opening the . Long blocks and footers go at the end.Mark up structure with tags, not with stylesSHR gives its own faces to h1..h6, b/strong, i/em, u, code, pre, blockquote, lists, mark and del/ins. Use them for what they mean, not for how they look:Real headings .. for hierarchy. Never a .// for lists and // for definitions. for quotes (it indents) and for preformatted blocks or ASCII art (it disables reflow). for inline code (fixed-width face).Watch out for the HTML5 semantic elements (article, section, nav, header, footer, main, aside, figure): they have no render of their own. They're treated as transparent containers and only contribute their textual content. They're fine for giving meaning to the document, but don't expect them to "show".Don't rely on CSS for anything essentialYou already know: only the inline style attribute is read, and only color, background-color, display and border-collapse. The rest is ignored. From that come three rules worth tattooing on yourself:Your page must be legible with CSS completely disabled. If it isn't, that's not an EWW problem, it's an HTML problem.Don't hide content with a display:none class. SHR won't apply it and that content will show up anyway. If you really need to hide something it would have to be style="display:none" inline, but that's bad practice. Better not to put that content in at all.Don't convey information with color alone. Even though inline color works, it demands enough contrast (that strict CIE DE2000 filter) and on poor terminals it isn't applied at all. A "required field in red" or a "green = correct" vanishes. Always back it up with text or symbols.And a consequence that sneaks in: spacing (margin, padding, line-height) doesn't exist. Separation comes from paragraph breaks. Structure with real tags, not with stray or empty divs.Tables: for data only, never for layoutSHR draws as a surprisingly good ASCII grid: it measures columns, distributes widths and even emulates colspan and rowspan. But it has its rules:Use them only for real data. A layout table produces an absurd, illegible grid.Watch the width. If the sum of the columns exceeds the frame, EWW just turns on truncate-lines and the experience degrades. Fewer columns and short cells fare much better.Images inside cells aren't embedded in the cell (a buffer limitation): they're inserted after the table. If order matters, avoid images in there.Images with alt and srcsetYes, Emacs shows images in graphical buffers. But don't get cocky:Always give a descriptive alt. It's what shows if the image is blocked, broken or if the user browses without images. In many EWW flows, the alt is the content.srcset is supported: EWW picks the right resolution for the frame width, so offer it several.Images load asynchronously over a placeholder, they don't block the text render. Don't rely on an image to communicate anything critical.data: URIs (base64 included) work, handy for small embedded icons.Forms that actually workForms are fine if they're pure HTML with action and method (GET or POST, including multipart/form-data for files):No JavaScript submissions (onclick, fetch). Use a real with its or .Put name on every field and value for the defaults. EWW collects by name.The recognized text types (text, password, email, number, date, color and textarea itself...) are painted as editable fields. The rest degrade to text. checkbox, radio and select work.Associate a with each field: keyboard navigation will thank you.Help the readable modeIf you want your article to look perfect with eww-readable, know your enemy. Its heuristic scores each node by word count, penalizes links (it subtracts their words), rewards images, and keeps the node with more than 100 words and the highest score.The practical takeaway? Wrap the body of the article in a single container with lots of continuous text and don't chop it up into a thousand tiny divs full of links. An or a with long paragraphs wins. A tangle of with navigation loses.Headers and metadata that do countFour small details that make a difference:: shown in EWW's header line. Always set it and make it descriptive.: EWW uses it as an encoding fallback. Declare UTF-8.: respected, useful for resolving relative links.HTTPS with a valid certificate: EWW colors the header-line title according to the TLS status. Serve your site over HTTPS.So, what is it good for?EWW is an excellent HTML document reader and a deliberately incomplete web browser. And once you accept that duality, it fits like a glove in a handful of scenarios:Reading without leaving Emacs: documentation, blogs, articles, wikis, HTML man pages... with all your usual keys and with isearch.Focused reading: the readable mode, with its word-density scoring, leaves the text and nothing else. No noise.Low bandwidth and zero distractions: no ads, no JS, no pop-ups, no telemetry. Accessibility out of the box.Feeds and mail with HTML: remember that SHR is what Gnus, elfeed and mu4e use underneath.A single flow: search and open links from Emacs itself, with bookmarks, history, multiple buffers or tabs and sessions.Scriptable: you parse the DOM from libxml-parse-html-region directly and automate whatever you want.Its philosophy is the opposite of a modern browser. Instead of emulating a graphical rendering engine, it translates semantic HTML into Emacs text. Anything that is a "document" works very well and very fast. Anything that is a "web application" doesn't work at all. And that's the beauty of it: it won't pass the Acid3 test, but not out of deficiency, out of design.Will you buy me a coffee? This is how I keep writing without ads or paywalls. Sure, it's on me!Send an email to comment+article-e0e00b4b@andros.dev to leave a comment. The subject will be ignored.