# Icon Recipes

> Copy-paste recipes for animated icons: buttons, a notification bell, a like button, a download button, icon navigation and a feature list.

Source: https://www.motion-components.dev/docs/recipes/icons/

Everyday interface patterns built with [`motion-icon`](https://www.motion-components.dev/docs/icons/motion-icon/) and [`motion-icon-state`](https://www.motion-components.dev/docs/icons/motion-icon-state/). Each snippet is complete: copy it into a page that loads the library.

```html
<script type="module" src="https://cdn.jsdelivr.net/npm/motion-components@1/+esm"></script>
```

## Button with an animated icon

Put a `motion-icon` inside a button. It animates when the pointer enters any part of the button, not only the icon.

```html
<button>
  <motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/send.svg" animation="bounce"></motion-icon>
  Send
</button>
```

## Notification bell

The bell wiggles when a notification arrives. Call `replay()` from your code, and update the badge and the button name for screen readers.

```html
<button id="notifications" aria-label="Notifications">
  <motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/bell.svg" animation="wiggle"></motion-icon>
  <span id="notification-count" class="badge" hidden>0</span>
</button>

<button id="simulate">Simulate a notification</button>

<script type="module">
  const button = document.querySelector('#notifications')
  const bell = button.querySelector('motion-icon')
  const badge = document.querySelector('#notification-count')
  let count = 0

  function notify() {
    count += 1
    badge.textContent = count
    badge.hidden = false
    button.setAttribute('aria-label', `Notifications, ${count} new`)
    bell.replay()
  }

  document.querySelector('#simulate').addEventListener('click', notify)
</script>

<style>
  .badge {
    min-width: 1.1rem;
    padding: 0 0.3rem;
    border-radius: 999px;
    background: #e11d48;
    color: #fff;
    font-size: 0.7rem;
    font-weight: 700;
  }
</style>
```

## Like button with a count

The heart fills on a spring and the count goes up or down. The button keeps `aria-pressed` in sync, so screen readers know the state.

```html
<button id="like" aria-pressed="false" aria-label="Like">
  <motion-icon-state name="heart"></motion-icon-state>
  <span id="like-count">128</span>
</button>

<script type="module">
  const button = document.querySelector('#like')
  const heart = button.querySelector('motion-icon-state')
  const count = document.querySelector('#like-count')

  button.addEventListener('click', () => {
    const liked = button.getAttribute('aria-pressed') !== 'true'
    button.setAttribute('aria-pressed', liked)
    heart.active = liked
    count.textContent = Number(count.textContent) + (liked ? 1 : -1)
  })
</script>
```

## Download button: arrow, spinner, check

Two components work together. `motion-icon` shows the arrow and bounces on hover. On click, `motion-icon-state` takes over with a spinner that turns into a check.

```html
<button id="download">
  <motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/download.svg" animation="bounce"></motion-icon>
  <motion-icon-state name="loading" hidden></motion-icon-state>
  Download
</button>

<script type="module">
  const button = document.querySelector('#download')
  const arrow = button.querySelector('motion-icon')
  const status = button.querySelector('motion-icon-state')
  const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms))

  button.addEventListener('click', async () => {
    button.disabled = true
    arrow.hidden = true
    status.hidden = false
    status.active = false   // spinner
    await wait(1500)        // replace with your download
    status.active = true    // check
    await wait(1500)
    status.hidden = true
    arrow.hidden = false
    button.disabled = false
  })
</script>

<style>
  motion-icon[hidden],
  motion-icon-state[hidden] {
    display: none;
  }
</style>
```

The `[hidden]` rule is needed because a component's own `display` style is stronger than the browser's default for `hidden`.

## Navigation with icons

Each link has an icon with its own animation. Inside a link, the icon animates when the pointer enters the whole link.

```html
<nav class="icon-nav">
  <a href="#home"><motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/house.svg"></motion-icon>Home</a>
  <a href="#inbox"><motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/inbox.svg" animation="bounce"></motion-icon>Inbox</a>
  <a href="#calendar"><motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/calendar.svg"></motion-icon>Calendar</a>
  <a href="#settings"><motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/settings.svg" animation="rotate"></motion-icon>Settings</a>
</nav>

<style>
  .icon-nav {
    display: grid;
    gap: 0.25rem;
    width: 12rem;
  }

  .icon-nav a {
    display: flex;
    align-items: center;
    gap: 0.6rem;
    padding: 0.5rem 0.75rem;
    border-radius: 8px;
    color: inherit;
    text-decoration: none;
  }

  .icon-nav a:hover {
    background: rgb(0 0 0 / 0.05);
  }
</style>
```

## Feature list that draws in on scroll

Each icon draws itself in when the list scrolls into view. A growing `delay` makes them follow each other.

```html
<ul class="feature-list">
  <li>
    <motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/zap.svg" trigger="view"></motion-icon>
    <span><strong>Light</strong> Each icon file loads once and is shared.</span>
  </li>
  <li>
    <motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/shield-check.svg" trigger="view" delay="0.15"></motion-icon>
    <span><strong>Accessible</strong> Reduced motion is respected.</span>
  </li>
  <li>
    <motion-icon src="https://cdn.jsdelivr.net/npm/lucide-static@1/icons/sparkles.svg" trigger="view" delay="0.3"></motion-icon>
    <span><strong>Simple</strong> One tag per icon.</span>
  </li>
</ul>

<style>
  .feature-list {
    display: grid;
    gap: 0.9rem;
    margin: 0;
    padding: 0;
    list-style: none;
    --mc-icon-size: 1.4rem;
  }

  .feature-list li {
    display: flex;
    align-items: center;
    gap: 0.75rem;
  }
</style>
```

## More recipes

The [motion-icon-state page](https://www.motion-components.dev/docs/icons/motion-icon-state/#recipes) has five more: a menu button, a copy button, a save button with a spinner, a show password button and an accordion.
