SVG Icons
Components are perfect for SVG icons - encapsulate the SVG code once, reuse it everywhere with customizable size and color.
Basic Icon Component¶
components/icons/icon-check.jinja
{#def size=24 #}
<svg
xmlns="http://www.w3.org/2000/svg"
width="{{ size }}"
height="{{ size }}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
{{ attrs.render(class="icon icon-check") }}
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
usage
{#import "icons/icon-check.jinja" as IconCheck #}
<IconCheck />
<IconCheck size="32" />
<IconCheck class="text-green" />
Generic Icon Wrapper¶
Create a base component that other icons extend:
components/icons/icon.jinja
{#def size=24 #}
<svg
xmlns="http://www.w3.org/2000/svg"
width="{{ size }}"
height="{{ size }}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
{{ attrs.render(class="icon") }}
>
{{ content }}
</svg>
components/icons/icon-x.jinja
{#import "./icon.jinja" as Icon #}
{#def size=24 #}
{% do attrs.set(size=size) %}
<Icon attrs={{ attrs }}>
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</Icon>
components/icons/icon-menu.jinja
{#import "./icon.jinja" as Icon #}
{#def size=24 #}
{% do attrs.set(size=size) %}
<Icon attrs={{ attrs }}>
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
</Icon>
Dynamic Icon Component¶
Load icons by name:
components/icon.jinja
{#def name, size=24 #}
{% set icons = {
"check": '<polyline points="20 6 9 17 4 12"></polyline>',
"x": '<line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line>',
"menu": '<line x1="3" y1="12" x2="21" y2="12"></line><line x1="3" y1="6" x2="21" y2="6"></line><line x1="3" y1="18" x2="21" y2="18"></line>',
"search": '<circle cx="11" cy="11" r="8"></circle><line x1="21" y1="21" x2="16.65" y2="16.65"></line>',
"user": '<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"></path><circle cx="12" cy="7" r="4"></circle>',
} %}
<svg
xmlns="http://www.w3.org/2000/svg"
width="{{ size }}"
height="{{ size }}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
{{ attrs.render(class="icon icon-" ~ name) }}
>
{{ icons.get(name, "") | safe }}
</svg>
usage
{#import "icon.jinja" as Icon #}
<Icon name="x" size="16" />
<Icon name="menu" class="text-gray-600" />
<Icon name="search" />
Icon Button¶
Combine icons with buttons:
components/icon-button.jinja
{#def label="" #}
{#css icon-button.css #}
{% do attrs.setdefault(type="button") %}
{% do attrs.set(aria_label=label if label else None)}
<button {{ attrs.render(class="btn btn--icon") }}>
{{ content }}
</button>
usage
{#import "btn--icon.jinja" as IconButton #}
{#import "icons/icon-x.jinja" as IconX #}
<IconButton label="Close" @click="close()">
<IconX size={{ 20 }} />
</IconButton>
Button with Icon and Text¶
components/button.jinja
{#def text="" #}
{#css button.css #}
{% do attrs.setdefault(type="button") %}
<button {{ attrs.render(class="btn") }}>
{% slot icon %}{% endslot %}
{% if text %}
<span>{{ text }}</span>
{% else %}
{{ content }}
{% endif %}
</button>
usage
{#import "button.jinja" as Button #}
{#import "icons/icon-check.jinja" as IconCheck #}
<Button text="Save">
{% fill icon %}
<IconCheck size="18" aria_hidden />
{% endfill %}
</Button>
Filled vs Stroke Icons¶
components/icons/icon-heart.jinja
{#def size=24, filled=false #}
<svg
xmlns="http://www.w3.org/2000/svg"
width="{{ size }}"
height="{{ size }}"
viewBox="0 0 24 24"
fill="{{ 'currentColor' if filled else 'none' }}"
stroke="currentColor"
stroke-width="2"
{{ attrs.render(class="icon icon-heart") }}
>
<path d="M20.84 4.61a5.5 5.5 0 0 0-7.78 0L12 5.67l-1.06-1.06a5.5 5.5 0 0 0-7.78 7.78l1.06 1.06L12 21.23l7.78-7.78 1.06-1.06a5.5 5.5 0 0 0 0-7.78z"></path>
</svg>
usage
<IconHeart /> {# Outline #}
<IconHeart filled /> {# Filled #}
<IconHeart filled class="text-red-500" />
Spinner Icon¶
components/icons/icon-spinner.jinja
{#def size=24 #}
{#css spinner.css #}
<svg
xmlns="http://www.w3.org/2000/svg"
width="{{ size }}"
height="{{ size }}"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
{{ attrs.render(class="icon icon-spinner") }}
>
<circle cx="12" cy="12" r="10" stroke-opacity="0.25"></circle>
<path d="M12 2a10 10 0 0 1 10 10" stroke-linecap="round"></path>
</svg>
spinner.css
.icon-spinner {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
Icon with Badge¶
components/icon-badge.jinja
{#def count=0 #}
{#css icon-badge.css #}
<span class="icon-badge-wrapper">
{{ content }}
{% if count > 0 %}
<span class="icon-badge">{{ count if count < 100 else "99+" }}</span>
{% endif %}
</span>
icon-badge.css
.icon-badge-wrapper {
position: relative;
display: inline-flex;
}
.icon-badge {
position: absolute;
top: -10px;
right: -10px;
background: rgba(255,0,0,0.8);
color: white;
font-size: 10px;
font-weight: bold;
padding: 2px 6px;
border-radius: 10px;
}
usage
{#import "icon-badge.jinja" as IconBadge #}
{#import "icons/icon-bell.jinja" as IconBell #}
<IconBadge count={{ notifications_count }}>
<IconBell />
</IconBadge>
42
Tips¶
- Use
currentColorfor fill/stroke to inherit text color - Set sensible defaults for size (24px is common)
- Add
aria-hidden="true"for decorative icons - Use
aria-labelon icon-only buttons - Keep SVGs optimized - remove unnecessary attributes