Like / Reaction Button

Free

Reaction button with 4 icon variants (heart, thumbs-up, star, clap), burst particle animation on toggle, and an optimistic count that rolls back when the write fails. 3 sizes.

</>sv-like likereactionbutton

Complexity tier: Interactive The primitive bar, plus declared async states, a keyboard model, focus restoration and undo for destructive actions.

import { SvLikeComponent } from '@styloviz/forms';

Demo & usage

Basic usage

Live, interactive instance.

When to use

  • Adding a lightweight reaction/like affordance to posts, comments or cards.
  • You want an optimistic count with a satisfying burst animation on toggle.
  • For multi-emoji reactions on a thread, use the Comment component's reactions.

Async states

  • error

Server writes

Writes
  • likeChange

Each payload carries a requestKey — send it with the request so a retry cannot apply the same change twice.

Rollback
Call event.rollback() when the save fails — the change is already on screen, and this is what puts it back.
While saving
Bind pending — with two writes outstanding there is no single state to roll back to, so the action stays locked until the first one lands.
On failure
Pass the failure to error to show it in place — without taking away what the user was working on.

Inputs

NameTypeDefaultDescription
countnumber (two-way)0The reaction count (two-way). A `model`, because the count is the thing the toggle changes: it moves the instant the button is clicked, and the host's next value — the number the server actually recorded — wins over whatever was shown optimistically.
likedboolean (two-way)falseWhether the current user has reacted (two-way: `[(liked)]`). Bind it two-way, not one-way. The button flips the state itself, and being a `model` is what pushes that back into YOUR signal, so the two never drift and a later correction is always a real change. It used to be a plain input mirrored into a `linkedSignal`, which only re-runs when its source **changes** — so a host undoing a rejected like by re-asserting `[liked]="false"` set it to the value it already held, nothing re-ran, and the like stayed on screen permanently. A one-way `[liked]` has the same flaw for the same reason: Angular skips a binding whose expression has not changed.
variantLikeVariant'heart'Icon variant.
sizeLikeSize'md'Size.
showCountbooleantrueShow the count next to the icon.
disabledbooleanfalseDisabled state.
pendingbooleanfalseTrue while the toggle is being written. The button stops responding, which is what keeps `rollback()` unambiguous — and it is also the answer to a double-tap producing two requests.
errorSvDataError | string | nullnullThe last write failed. Rendered as a danger ring, a triangle glyph and the message as visible text under the button — tied to it with `aria-describedby` — so the state is not conveyed by colour alone. Roll the value back as well — this input reports the failure, it does not undo it.
signedInbooleantrueWhether the viewer is signed in. When false the button does not toggle; it emits `authRequired` so the host can prompt for sign-in. A reaction control shown to a signed-out user had no defined behaviour: it toggled, looked saved, and the write it reported could not succeed.

Outputs

EventPayload typeDescription
likeChangeLikeToggleEventThe user toggled the reaction.
authRequiredvoidA signed-out viewer tried to react.
countChangenumberEmits the new `count` whenever it changes — the output half of `[(count)]`.
likedChangebooleanEmits the new `liked` whenever it changes — the output half of `[(liked)]`.

Customization & theming

  • Switch icon with `variant`: `heart`, `thumbs-up`, `star` or `clap`.
  • Control size with `size` (sm, md, lg).
  • Hide the counter by setting `[showCount]="false"`.
  • Seed initial state with `[liked]="true"`; bind both `[(liked)]` and `[(count)]` two-way so the server's numbers win over the optimistic guess.
  • `[error]` accepts a string or an `SvDataError` — the message renders as visible "Not saved: …" text under the button, announced through a polite live region and linked to the button with `aria-describedby`.

Best practices

  • Do: Call `rollback()` from the `likeChange` payload when the write is rejected — the button has already moved, and nothing else puts it back.
  • Do: Send `requestKey` with the write and ignore a key you have already applied, so a retry cannot count twice.
  • Do: Bind `[pending]` while the write is in flight. It stops a double-tap sending two conflicting requests, and it is what keeps `rollback()` unambiguous.
  • Do: Seed `[liked]` and `[count]` from the current user's state on load.
  • Don't: Do not leave a reaction button interactive for a signed-out viewer. Bind `[signedIn]="false"` and handle `(authRequired)`, or the toggle looks saved and cannot be.
  • Tip: Pick a `variant` (heart/thumbs-up/star/clap) that matches the content's tone.

Accessibility

  • A native `<button>` with `aria-pressed` reflecting the toggled state, and an accessible name that carries the count and "not saved" after a failed write.
  • A failed write is a danger ring, a triangle glyph and visible message text — the button's `aria-describedby` points at that text, so the reason is read once as its description — never colour alone.
  • While `[pending]`, the button sets `aria-busy` and stops responding.
  • The burst is `motion-safe:` only and is not rendered at all under `prefers-reduced-motion` (with `motion-reduce:hidden` covering a preference that changes mid-burst), so reduced motion gets no particles rather than four dots parked on the button.
  • Disabled state sets `aria-disabled` and blocks interaction.

Feedback on Like / Reaction Button

Spotted a gap in this page, or something that didn't work as described? Comments are GitHub Discussions in the public styloviz/community repo, so posting needs a GitHub account.

Loads giscus.app and GitHub — nothing is fetched until you click.