neat-annotations: Hand-Drawn CSS Annotations Without JavaScript
Hand-drawn CSS annotations for inline content
At a glance
- What is it?
- A single CSS file that draws hand-drawn arrows and handwritten labels around inline content. It is a visual layer only, and its positioning model has real costs.
- Who is it for?
- Adopt neat-annotations for marketing pages, documentation callouts, and demo sites where a hand-drawn arrow over a sentence is decoration rather than instruction. Do not adopt it where the label carries meaning that assistive technology must receive, because the project's own accessibility note says data-note must not be the only source of instructions, status or validation.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 4 days ago.
- What is it written in?
- Mainly HTML, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on September 21, 2026, and from our analysis. They are not legal advice.
DEEP OPEN-SOURCE ANALYSIS
The problem neat-annotations solves, and who it is for
Most pages that want a hand-drawn arrow pointing at a phrase end up with an image. A PNG or SVG of a squiggle is fixed in size, does not reflow with the text it points at, and has to be redrawn whenever the copy changes. neat-annotations replaces that image with CSS: you wrap the phrase in a span, add a class, and the arrow and label are generated from the stylesheet. The README describes it as "Hand-drawn arrows and handwritten labels for your website. Pure CSS, no JavaScript, no build step, one self-contained file."
The audience is narrow and specific. It is for people writing static sites, documentation, landing pages or internal demos who can edit HTML but do not want a component library or a bundler in the loop. The repository's top level is CNAME, LICENSE, README.md, assets/, index.html, neat-annotations.css, render-previews.html and social-preview.png. There is no package.json, no build configuration, no test directory. That layout is the pitch: the deliverable is one stylesheet, and index.html is the demo page behind the project's homepage.
If your annotations need to be interactive, dismissible, or generated at runtime from data, this is the wrong shape of tool. It renders decoration, not behaviour.
How the arrow and label are produced from a class name
The mechanism is a span with two or three classes and a data attribute. The base class is ann. A direction class, one of ann-n, ann-ne, ann-e, ann-se, ann-s, ann-sw, ann-w or ann-nw, selects where the arrow points. The README states that "a direction class names where the arrow points. For example, ann-n places the label below the target and points the arrow north toward it." The label text comes from data-note.
Everything else is a CSS custom property. The fine-tuning table lists --ann-color for arrow and label colour, --ann-mark for the target highlight, --ann-font for the label font, --ann-target-gap at 5px, --ann-label-gap at 6px, --ann-lower-label-gap at -4px, --ann-label-max-width at 150px, --ann-arrow-x and --ann-arrow-y at 0px, --ann-text-x at 0px and --ann-text-y at 5px, and --ann-rotate at -4deg. The rotate default is why the labels look hand-placed rather than typeset. You can set any of these inline on the element, which means a single annotation can be nudged without touching the stylesheet.
The layout model is the part worth understanding before you adopt it. The README states plainly: "annotations are positioned outside their target and do not reserve space. Leave enough margin around annotated lines for the arrow and label." Nothing in the box model grows to accommodate the label. If your paragraph sits tight against a container edge, the annotation will overlap whatever is next to it. This is a deliberate trade: reserving space would push your text around and make the annotation part of the flow rather than something drawn on top of it.
Installing neat-annotations and writing a first annotation
There is no package manager step. The README gives two options: link the stylesheet from jsDelivr, or download neat-annotations.css and serve it locally. The CDN link is the fastest way to see it working.
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/syabro/neat-annotations/neat-annotations.css">With that in place, wrap the phrase you want to point at. The README's own example annotates a sentence about a dashboard, using ann-n to point north and data-note for the label text.
<span class="ann ann-n ann-amber" data-note="no refresh needed">in real time</span>You should see the phrase highlighted, a hand-drawn arrow rising from below it, and the note text in a handwritten style. The colour comes from ann-amber, one of six built-in classes: ann-amber, ann-blue, ann-green, ann-red, ann-purple and ann-rainbow. The default with no colour class is warm gray.
The handwriting is optional. The README says Shantell Sans is optional and that "otherwise labels fall back to a cursive font". To match the demo, load it from Google Fonts.
<link href="https://fonts.googleapis.com/css2?family=Shantell+Sans:wght@400;500;600&display=swap" rel="stylesheet">If you want the highlight without any arrow or label, the README's instruction is to omit both data-note and the direction class. A bare ann plus a colour class becomes a text marker. If the target already has its own background, add ann-no-mark so the annotation does not paint over it.
Where the positioning model breaks
The no-space-reservation rule is the first failure mode. An annotation on a line near the top of a section will draw its label upward into whatever precedes it. The README's answer is margin, which means the cost of using the library is paid in layout, not in JavaScript weight. On a dense page with tight vertical rhythm, adding the required margin changes the design.
The second constraint is label width. --ann-label-max-width defaults to 150px, and the README notes that long notes wrap according to it. A 150px label is roughly a short phrase. Anything longer turns into a stack of lines whose height is not accounted for by the surrounding layout, which loops back into the overlap problem. You can raise the variable per element, but the README does not document a global override, so wide labels have to be handled annotation by annotation.
The third is accessibility, and the project is direct about it. The README states: "Annotations are visual enhancements. Do not use data-note as the only source of instructions, status, validation, or other essential information. Repeat important content in visible HTML or connect a real description to the target with aria-describedby." That is not a caveat bolted on at the end; it rules out the most tempting use of the library. A validation message delivered through data-note is not delivered at all. The README also does not document rollback or removal, because there is nothing to roll back: deleting the stylesheet link removes every annotation on the page, and the spans become ordinary inline text.
neat-annotations against a JavaScript annotation library
The obvious alternative is a JavaScript annotation or callout library, the kind that renders tooltips, popovers and positioned callouts from a config object. The difference is not cosmetic. A script-driven library measures the target at runtime, computes coordinates, and can reposition on scroll or resize, which is how it avoids the overlap problem. It also usually exposes events, so a callout can open, close and be focused by keyboard.
neat-annotations gives up all of that. In exchange it has no runtime, no hydration step, no dependency to keep current, and nothing that can fail after the page has loaded. The annotations are in the first paint. For a static site or a documentation page where the arrow is a stylistic device over a sentence, that trade is clearly correct. For an interface where a callout must track a moving element, or where the label must be reachable by keyboard, the CSS approach has no mechanism to offer and you should use the scripted library instead.
The colour system is where the project does something the alternative often does not. Six built-in classes change "the arrow, label, and target highlight together", and --ann-color accepts any CSS colour directly, as in the README's example of setting --ann-color to #ff1493. ann-rainbow animates through hues and, per the README, respects prefers-reduced-motion. That is a small detail, but it is the kind of thing that is usually forgotten.
Maintenance, licence and what upgrading actually involves
The repository is not archived, and the last push was on 2026-07-22. No releases were retrieved, which fits a project distributed as a file rather than a package: there is no version number to pin and no changelog to read. Upgrading means replacing neat-annotations.css, or changing nothing at all if you are loading it from jsDelivr, in which case you receive whatever is on the branch at request time. That last point deserves attention. A CDN link that resolves against a branch is convenient and also means the file can change underneath a deployed site. Downloading the file and serving it locally, which the README offers as the alternative, removes that exposure at the cost of manual updates.
The licence is MIT, stated in the README and present as a LICENSE file at the repository root. MIT permits commercial use, modification and redistribution provided the copyright notice and permission notice are retained. This is a description of the licence text, not legal advice; if you are redistributing the file inside a product, read LICENSE yourself.
Maintenance cost is genuinely low, and that is a property of the design rather than a promise. There is no dependency tree to audit, no build output to regenerate, no runtime to patch. The variables are CSS custom properties, so a browser that does not support them degrades the annotation rather than breaking the page.
Editorial conclusion
Adopt neat-annotations for marketing pages, documentation callouts, and demo sites where a hand-drawn arrow over a sentence is decoration rather than instruction. Do not adopt it where the label carries meaning that assistive technology must receive, because the project's own accessibility note says data-note must not be the only source of instructions, status or validation. Before committing, verify two things on a real page: that the surrounding margin absorbs the arrow and label without overlap, and that your label text fits the default 150px wrap width or that you have set --ann-label-max-width deliberately.
Frequently asked questions
What is an example of a good annotation with neat-annotations?
The README's own example wraps a phrase and points at it from below: a span with classes ann, ann-n and ann-amber, plus data-note containing the label text. The label reads "no refresh needed" and the arrow points north toward the highlighted words.
What are the four types of annotations in neat-annotations?
The project does not group annotations into four types. It documents eight direction classes (ann-n, ann-ne, ann-e, ann-se, ann-s, ann-sw, ann-w, ann-nw), six colour classes, a highlight-only form with no direction or data-note, and ann-no-mark for targets that already have their own fill.
What are the five types of annotations in neat-annotations?
There is no five-type classification in the documentation. The README organises the API by direction, by colour, by the highlight-only case, and by the ann-no-mark modifier, with fine-tuning handled through CSS custom properties such as --ann-color and --ann-label-max-width.
Community notes