# Icon

> One inline SVG, from path data or child shapes, with a required label that makes the accessibility choice explicit: null hides it, a string names it. 1em by default, five fixed steps, fill or stroke, currentColor.

- ID: `cmp_icon_01`
- Slug: `icon-01`
- Version: `1.0.0` (current)
- Status: published
- Published: 2026-10-01T08:13:34Z
- Updated: 2026-10-01
- Available versions: `1.0.0`
- Kind: control
- Primary category: `typography`
- Detail page: https://pagesugar.com/components/icon-01?variant=default
- Preview: https://pagesugar.com/preview/icon-01

## Variants

| Variant | Label | Default | Artifact digest |
| --- | --- | --- | --- |
| `default` | Inherited colour | yes | `sha256-0e26d41df6f16cf6ee221800fc4909ea3f42fd70b0b567fc015a5973894c7aea` |

## Runtime and compatibility

- Runtime: svelte
- Svelte: 5
- SvelteKit required: no (portable Svelte component)
- Tailwind CSS: 4
- SSR: supported
- Requires client-side JavaScript: no
- Integration level: presentational
- Appearance modes: light
- Suggested directory: `src/lib/components/icon-01`

## Dependencies

No third-party runtime packages.

## Services

No external services required.

## Usage

Pass label (a name, or null for decorative) and either path data or child shapes. It renders one inline svg in currentColor at 1em or a fixed step. It does not ship an icon set, fetch SVG files, accept raw SVG strings, add tooltips or make the icon focusable.

Required props: `label`

```svelte
<script lang="ts">
	import Icon from '$lib/components/icon-01/Icon.svelte';

	const check = 'M5 12.5l4.5 4.5L19 7.5';
</script>

<p>
	<Icon label={null} path={check} strokeMode /> Guests can comment without a seat
</p>

<button type="button" class="inline-flex items-center gap-2">
	<Icon label={null} size="sm" strokeMode>
		<path d="M10 11.5a3.75 3.75 0 1 0 0-7.5 3.75 3.75 0 0 0 0 7.5Z" />
		<path d="M3.5 20a6.5 6.5 0 0 1 13 0M19 8v6M16 11h6" />
	</Icon>
	Invite a guest
</button>

<Icon label="Overdue" size="sm" path="M12 3 22 20H2Z" class="text-amber-700" />
```

Limitations:

- No icons are included. Paste path data from your own drawings or a set whose licence allows it, or install an icon package and pass its shapes as children.
- Path data is a single d attribute. Icons that need several elements, a fill-rule or mixed fill and stroke go in children instead.
- A filled icon with holes must draw its counters in the opposite direction, or pass fill-rule="evenodd" on a child path, because the svg uses the default nonzero rule.
- The fixed steps set an on-screen stroke weight and convert it through the viewBox's longer side, so sets drawn on 16 or 20 unit grids keep the same weight. Their own optical sizes are not chosen for you.
- Mirroring flips the drawing horizontally under dir="rtl"; icons whose meaning does not depend on direction (a check, a clock) should not pass mirror.

## Usage guide

### Icon

`label` is required, and it is the whole point: every icon on the page has made its
accessibility decision in the markup. `null` hides the icon from assistive technology; a string
names it.

```svelte
<script lang="ts">
	import Icon from '$lib/components/icon-01/Icon.svelte';

	const clock = 'M12 21a9 9 0 1 0 0-18 9 9 0 0 0 0 18ZM12 7.5V12l3 2';
	const warning = 'M12 3 22 20H2Z';
</script>

<!-- Decorative: the text beside it says the same thing. -->
<p class="text-sm text-zinc-600">
	<Icon label={null} path={clock} strokeMode /> Due Thursday 9 October
</p>

<!-- Meaningful: nothing else says "Overdue". -->
<Icon label="Overdue" size="sm" path={warning} class="text-amber-700" />
```

#### Which one to choose

| The icon…                                     | label       | Renders                                   |
| --------------------------------------------- | ----------- | ----------------------------------------- |
| sits beside text that says the same thing     | `null`      | `aria-hidden="true"`, `focusable="false"` |
| is the only child of a button or link         | `null`      | the same; name the button instead         |
| carries meaning nothing else on the page does | `"Overdue"` | `role="img"`, `aria-label="Overdue"`      |

An icon-only button names the button, not the icon:

```svelte
<button type="button" aria-label="Close" class="grid size-11 place-items-center rounded-lg">
	<Icon label={null} size="md" path="M6 6l12 12M18 6 6 18" strokeMode />
</button>
```

#### Sizes

| size      | Pixels | Stroke weight | Use                                  |
| --------- | ------ | ------------- | ------------------------------------ |
| (omitted) | 1em    | 1/12 of grid  | inside text: paragraphs, headings    |
| `xs`      | 12     | 1.25 px       | captions, badges                     |
| `sm`      | 16     | 1.75 px       | buttons, dense rows, beside 13–14 px |
| `md`      | 20     | 1.5 px        | navigation, lists                    |
| `lg`      | 24     | 1.5 px        | illustrations, empty states          |
| `xl`      | 32     | 2 px          | large illustrations                  |

Stroke weights are on-screen pixels, converted through the longer side of the `viewBox`, so a
set drawn on a 20 or 16 unit grid gets the same weight when it passes its own `viewBox`. A 1em icon drops by
0.125em so it centres on the capitals of the text around it rather than sitting on the baseline.

#### Beside text that wraps

Centre the icon on the first line, not on the paragraph. Put it in a box exactly as tall as one
line of the text beside it (`h-6` for `text-base/6`):

```svelte
<p class="flex gap-3 text-base/6">
	<span class="flex h-6 items-center text-lg">
		<Icon label={null} path="M5 12.5l4.5 4.5L19 7.5" strokeMode />
	</span>
	<span>Guests can comment on milestones and follow the timeline without taking a paid seat.</span>
</p>
```

#### Several shapes

Pass children when an icon needs more than one element, a `fill-rule`, or a filled part inside
an outline icon:

```svelte
<Icon label={null} strokeMode>
	<path d="M6 5h12a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V7a2 2 0 0 1 2-2ZM4 10h16" />
	<path d="M8 13h3v3H8Z" fill="currentColor" stroke="none" />
</Icon>
```

Children are drawing only. A `<title>`, link or `tabindex` inside them would add a tooltip or a
focus stop the icon otherwise never has.

#### Right to left

Arrows and chevrons pass `mirror` and flip when their resolved direction is right to left (a
`dir="ltr"` island inside an Arabic page stays unflipped). Icons whose meaning has no
direction (a check, a clock, a calendar) leave it off.

#### Alignment

A 1em icon sits at `vertical-align: -0.125em`, a fixed step at `middle`. Override with an
important utility, `class="align-baseline!"`: two alignment utilities on the same element are
resolved by Tailwind's order, not by which one you wrote last.

## Props

| Name | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `label` | string \| null | yes |  | Accessible name, always passed. null marks the icon decorative (aria-hidden). A string makes it role="img" with that aria-label. A blank string is treated as null. |
| `path` | `string` | no |  | Path data (the d attribute) for a single-path icon. When set, children are ignored. |
| `children` | `Snippet` | no |  | SVG child elements (paths, circles, groups) for a multi-shape icon. Used when path is absent. |
| `viewBox` | `string` | no | `'0 0 24 24'` | The icon's drawing grid, four numbers. Its longer side converts each step's stroke weight into grid units. |
| `size` | 'xs' \| 'sm' \| 'md' \| 'lg' \| 'xl' | no |  | Fixed square step: 12, 16, 20, 24 or 32 px. Omitted, the icon is 1em and scales with the surrounding text. |
| `strokeMode` | `boolean` | no | `false` | Draw with a round-capped, round-joined stroke in currentColor and no fill, for outline icon sets. |
| `mirror` | `boolean` | no | `false` | Flip the icon horizontally in right-to-left text. For arrows, chevrons and other directional icons. |
| `class` | `string` | no |  | Extra classes on the svg: a text colour, margins, or a size-\* class that replaces the step. |

## Customization

The icon has no colour of its own: it is drawn in currentColor, so set the text colour on it or any ancestor. Size through size or a size-\* class; stroke weights live in the STROKE map.

- Colour: pass class="text-amber-700" or set color on a parent. On a dark band or a cream page the icon follows the text with no change.
- Size: omit size inside text so the icon is 1em and grows with a heading. Use sm (16 px) beside 13 to 14 px labels in buttons and dense rows, md (20 px) in navigation and lists, lg (24 px) only as an illustration.
- Any size: pass class="size-10" (or any size-\* utility). The steps are width and height attributes, so a class always wins. Stroke weight then scales with the drawing.
- Stroke weight: the STROKE map at the top of the script holds each step's weight in pixels (1.25 at 12 px, 1.75 at 16, 1.5 at 20 and 24, 2 at 32). Change it there; 1em icons draw 2 units on a 24-unit grid.
- Beside wrapping text: put the icon in a box one line tall (display: flex; align-items: center; height equal to the text's line height) so it stays level with the first line. usage.md has the pattern.
- Icon sets: paste a single d attribute into path, or pass the set's child elements as children. A set drawn on a 20 or 16 unit grid passes its own viewBox.
- Alignment: a 1em icon sits at vertical-align -0.125em and a fixed step at middle. To change it, pass an important utility such as class="align-baseline!", because two alignment utilities on one element resolve by Tailwind's order, not by which came last.

No public CSS variables.

## Accessibility

- label is a required prop typed string | null, so leaving out the accessibility decision is a TypeScript error rather than a silent default.
- Decorative (label={null}): aria-hidden="true" and focusable="false", no role. Use it whenever adjacent text already says the same thing.
- Meaningful (label="Overdue"): role="img" and aria-label on the svg. The component renders no \<title\>, so there is no hover tooltip and no duplicated name.
- An icon alone inside a button or link is decorative; give the button its name (aria-label or visually hidden text), not the icon. usage.md shows both.
- The icon is never focusable and carries no tooltip. A meaningful icon whose name should be visible on hover needs its own button or a visible label next to it.
- Drawn in currentColor, so forced-colours mode repaints it with the system text colour, and contrast follows the text around it. A meaningful icon given its own colour needs 3:1 against the background.
- Directional icons can pass mirror so they point the right way in right-to-left text.
- Children are rendered as given. Pass drawing elements only (paths, circles, groups): a \<title\>, a link or a tabindex inside them would bring back a tooltip or a focus stop that the svg itself never has.

Known limitations:

- A blank label is treated as decorative rather than reported; check that dynamic labels are never empty when the icon carries meaning.
- Colour alone is not a status: a red or amber icon still needs its label, or text beside it.

## License

- Declared source: MIT
- Default license approval is pending. See https://pagesugar.com/docs/license.

## Source

- Palette: Inherited colour (`default`)
- Entry: `Icon.svelte`
- Suggested directory: `src/lib/components/icon-01`
- Files: 1
- Artifact digest: `sha256-0e26d41df6f16cf6ee221800fc4909ea3f42fd70b0b567fc015a5973894c7aea`

Paths below are relative to the suggested directory. Copy the files as they are;
they import nothing from this site.

#### `Icon.svelte`

Role: entry · 3362 bytes · SHA-256 `4aa22533dd71c398ca70ad72eab003f06d160b0ecdccad097f2e680eb0501cf6`

```svelte
<script lang="ts" module>
	export type IconSize = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
</script>

<script lang="ts">
	import type { Snippet } from 'svelte';

	interface Props {
		/**
		 * Accessible name, and it is required. null marks the icon decorative (hidden from
		 * assistive technology); a string makes it an image with that name.
		 */
		label: string | null;
		/** Path data for a single-path icon: the d attribute. */
		path?: string;
		/** SVG child elements for a multi-shape icon. Used when path is absent. */
		children?: Snippet;
		/** The icon's drawing grid. */
		viewBox?: string;
		/** A fixed square step. Omitted, the icon is 1em and follows the text around it. */
		size?: IconSize;
		/** Draw with a round-capped stroke and no fill, for outline icon sets. */
		strokeMode?: boolean;
		/** Mirror in right-to-left text, for directional icons such as arrows and chevrons. */
		mirror?: boolean;
		/** Extra classes on the svg: colour, margins. */
		class?: string;
	}

	let {
		label,
		path,
		children,
		viewBox = '0 0 24 24',
		size,
		strokeMode = false,
		mirror = false,
		class: className
	}: Props = $props();

	/*
	 * Fixed steps in pixels, written as width and height attributes rather than size classes so a
	 * size-* class passed in through class always wins.
	 */
	const PIXELS: Record<IconSize, number> = { xs: 12, sm: 16, md: 20, lg: 24, xl: 32 };

	/*
	 * Stroke weight on screen at each step. 16 px icons sit beside 13 and 14 px labels at weight
	 * 500, so they draw heavier relative to their size than the 20 and 24 px steps do. At 12 px a
	 * heavier line closes the counters, so xs steps back to 1.25.
	 */
	const STROKE: Record<IconSize, number> = { xs: 1.25, sm: 1.75, md: 1.5, lg: 1.5, xl: 2 };

	const step = $derived(size && size in PIXELS ? size : undefined);
	const name = $derived(typeof label === 'string' && label.trim() ? label.trim() : null);

	/*
	 * Stroke width is in viewBox units. A square viewport fits the grid by its longer side, so
	 * convert the on-screen weight through that side. A malformed viewBox falls back to 24.
	 */
	const grid = $derived.by(() => {
		const [, , w, h] = viewBox
			.trim()
			.split(/[\s,]+/)
			.map(Number);
		const side = Math.max(w, h);
		return Number.isFinite(side) && side > 0 ? side : 24;
	});
	const strokeWidth = $derived(
		+(step ? (STROKE[step] * grid) / PIXELS[step] : grid / 12).toPrecision(4)
	);
	const dimension = $derived(step ? PIXELS[step] : '1em');
</script>

<svg
	xmlns="http://www.w3.org/2000/svg"
	{viewBox}
	width={dimension}
	height={dimension}
	fill={strokeMode ? 'none' : 'currentColor'}
	stroke={strokeMode ? 'currentColor' : undefined}
	stroke-width={strokeMode ? strokeWidth : undefined}
	stroke-linecap={strokeMode ? 'round' : undefined}
	stroke-linejoin={strokeMode ? 'round' : undefined}
	role={name ? 'img' : undefined}
	aria-label={name ?? undefined}
	aria-hidden={name ? undefined : 'true'}
	focusable="false"
	class={[
		'inline-block shrink-0',
		// 1em icons drop by an eighth of the text so they centre on the capitals, not the baseline.
		step ? 'align-middle' : 'align-[-0.125em]',
		// :dir() reads the resolved direction, so an ltr island inside an rtl page is not flipped.
		mirror && '[&:dir(rtl)]:-scale-x-100',
		className
	]}
>
	{#if path}
		<path d={path} />
	{:else if children}
		{@render children()}
	{/if}
</svg>
```

## Artifacts

### Inherited colour (`default`) (default)

- Artifact digest: `sha256-0e26d41df6f16cf6ee221800fc4909ea3f42fd70b0b567fc015a5973894c7aea`
- Entry: `Icon.svelte`
- Receipt: https://pagesugar.com/artifacts/cmp_icon_01/1.0.0/default/sha256-0e26d41df6f16cf6ee221800fc4909ea3f42fd70b0b567fc015a5973894c7aea/manifest.json
- Bundle: https://pagesugar.com/artifacts/cmp_icon_01/1.0.0/default/sha256-0e26d41df6f16cf6ee221800fc4909ea3f42fd70b0b567fc015a5973894c7aea/bundle.zip (4501 bytes, sha256 `674ffea1d48be41efc8017f50e5d7a3734f3b8650a2c595b2201ced18e5b475d`)

Files:

- `Icon.svelte` (entry, 3362 bytes): https://pagesugar.com/artifacts/cmp_icon_01/1.0.0/default/sha256-0e26d41df6f16cf6ee221800fc4909ea3f42fd70b0b567fc015a5973894c7aea/source/Icon.svelte
