Skip to content
omaris

Foundations

Shape

The Material 3 shape library, as a box you can put things in.

import { Shape } from 'omaris'
Learn

Examples

Basic

A shape is a box that clips what is in it — a colour, an icon, a photo. Give it a size and a background, and it is done.

Erbil
<script lang="ts">	import { Shape } from 'omaris';</script>​<Shape shape="cookie-9" size="5rem" class="bg-primary" /><Shape shape="clover-4" size="5rem" class="bg-tertiary" /><Shape shape="arch" size="5rem" src="/docs/photos/erbil.svg" alt="Erbil" /><Shape shape="burst" size="5rem" class="bg-tertiary-container text-tertiary-container-foreground">	<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">		<path d="m12 3 2.6 6.1 6.4.5-4.9 4.2 1.5 6.2L12 16.8 6.4 20l1.5-6.2L3 9.6l6.4-.5z" />	</svg></Shape>

Library

All thirty-six of them. Pick one and the big shape travels there from wherever it is — every pair morphs, because the morph never looks at the path data. shapeNames and shapePaths are exported too, if you want the outline without the component.

cookie-9
<script lang="ts">	import { Shape, Text, shapeNames, type ShapeName } from 'omaris';​	let picked = $state<ShapeName>('cookie-9');</script>​<div class="flex w-full flex-col items-center gap-6">	<Shape shape={picked} size="9rem" class="bg-primary" />	<Text variant="label-lg" tone="muted">{picked}</Text>​	<div class="grid w-full grid-cols-6 gap-3 sm:grid-cols-9 md:grid-cols-12">		{#each shapeNames as name (name)}			<button				type="button"				onclick={() => (picked = name)}				aria-label={name}				aria-pressed={picked === name}				class="rounded-shape-sm p-1 outline-offset-2 focus-visible:outline-2 focus-visible:outline-primary"			>				<Shape					shape={name}					hover="circle"					size="2.25rem"					class={picked === name ? 'bg-primary' : 'bg-muted-foreground/35'}				/>			</button>		{/each}	</div></div>

Morphing

hover and press are shapes, not states: the outline travels to them and back, on MD3's emphasized curve. Interrupt one halfway — leave while it is still moving — and it turns around from where it is rather than jumping.

<script lang="ts">	import { Shape } from 'omaris';</script>​<Shape shape="square" hover="cookie-12" size="6rem" class="bg-primary" /><Shape shape="heart" hover="pixel-circle" size="6rem" class="bg-tertiary" /><Shape shape="pill" hover="burst" press="circle" size="6rem" class="bg-primary-container" /><Shape shape="circle" hover="clover-4" duration={1200} size="6rem" class="bg-tertiary-container" />

Media

The reason to have these: a picture in something other than a rounded rectangle. src clips the image itself, so object-cover and the shape are one thing, and the morph on hover leaves the photo exactly where it was.

Erbil
Erbil
Duhok
Duhok
Basra
Basra
Najaf
Najaf
<script lang="ts">	import { Shape, Text } from 'omaris';​	const PLACES = [		{ name: 'Erbil', shape: 'cookie-7', hover: 'circle' },		{ name: 'Duhok', shape: 'clover-4', hover: 'cookie-12' },		{ name: 'Basra', shape: 'arch', hover: 'pill' },		{ name: 'Najaf', shape: 'ghostish', hover: 'square' }	] as const;</script>​{#each PLACES as place (place.name)}	<figure class="flex flex-col items-center gap-2">		<Shape			shape={place.shape}			hover={place.hover}			size="7rem"			src="/docs/photos/{place.name.toLowerCase()}.svg"			alt={place.name}		/>		<Text as="figcaption" variant="label-md" tone="muted">{place.name}</Text>	</figure>{/each}

Loading

cycle walks a list of shapes forever, and spin turns the outline while the contents counter-rotate on the same clock — which is why the tick inside the last one never leans. Together they are MD3's loading indicator; both stop under prefers-reduced-motion.

Deploying…
<script lang="ts">	import { Shape, Text } from 'omaris';</script>​<div class="flex items-center gap-3">	<Shape		cycle={['cookie-9', 'clover-4', 'pill', 'soft-burst']}		spin={6}		interval={900}		size="2.5rem"		class="bg-primary"	/>	<Text variant="label-lg">Deploying…</Text></div>​<Shape shape="flower" spin={12} size="2.5rem" class="bg-tertiary" />​<Shape	cycle={['circle', 'square', 'diamond', 'hexagon']}	size="2.5rem"	class="bg-primary-container"/>​<Shape	shape="cookie-12"	spin={8}	size="2.5rem"	class="bg-success-container text-success-container-foreground">	<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4">		<path d="m5 13 4 4 10-10" stroke-linecap="round" stroke-linejoin="round" />	</svg></Shape>

Overridden

The overridden case. A size-* class beats the size prop; a box that is not square stretches the outline with it, on purpose — the clip is in the box's own space. classes reaches the parts: here the picture is pushed around inside a shape that has not moved.

Kirkuk
<script lang="ts">	import { Shape } from 'omaris';</script>​<Shape shape="cookie-9" size="4rem" class="size-28 bg-primary" /><Shape shape="cookie-9" size="4rem" class="h-20 w-44 max-w-full bg-tertiary" /><Shape	shape="arch"	size="7rem"	src="/docs/photos/kirkuk.svg"	alt="Kirkuk"	classes={{ image: 'scale-150 object-left' }}/><Shape shape="pill" class="size-20 bg-linear-to-br from-primary to-tertiary shadow-3" />

When to use it

Use it for

  • A picture that should not be a rounded rectangle: a profile, a place, a category tile, a cover. src clips the image itself.
  • Decorative marks with a brand's personality: an empty state, an onboarding step, a badge behind an icon. <Shape> with a background class and no children is a filled shape.
  • A loading indicator that is not a spinner: cycle with spin. The contents counter-rotate, so an icon inside stays upright.
  • Feedback on a tile people can point at. hover and press are shapes, and the outline morphs to them.
  • The outline on its own, for a mask or a CSS clip-path elsewhere. shapePaths and shapeNames are exported, each a closed path in a 100×100 box.

Not for

  • The corner rounding of ordinary UI, like cards, buttons and sheets → the shape scale (rounded-shape-md, rounded-shape-xl).
  • A person's picture with a presence dot, initials fallback and a group stack → Avatar. Use Shape only when the frame itself must be unusual.
  • Progress with a number → Progress or Gauge. A cycling shape says "working", not "62%".
  • Cropping or zooming a photo a person picked → Image Cropper.
  • Text. Clipping a paragraph cuts lines mid-word.

Do

  • Keep the box square unless the stretch is the point. A wide box gives a wide shape.
  • Size it with the size prop, or a size-* class when the layout owns the size. The class wins.
  • Use hover on something already interactive (a link, a button, a card), so the movement is a response.
  • Pair a strong shape with a plain one. cookie-12 next to arch reads as a set; twelve different shapes read as noise.
  • Give an image an alt, or leave it empty when the picture is decoration.
  • Let a cycle run at 700–1200ms per shape. Faster and the morph never lands; slower and it looks stalled.

Don't

  • Morph on every state change in a list. One shape moving is fine; twenty is a screen that will not sit still.
  • Put a control at the edge of a concave shape. A burst's points leave the middle of each edge cut away.
  • Use spin on something people read, like a photo of a person. It is for a loader or a mark.
  • Use pixel-circle or heart in a dashboard because they exist. circle, square, arch and pill survive a redesign.
  • Set a clip-path in class. The component already owns that property on the root.

API

Shape

The Material 3 shape library, as a box you can put things in.

Thirty-six outlines — cookies, clovers, bursts, an arch, a heart — that clip whatever is inside them: a photo, an avatar, an icon, a flat block of colour. They are the decorative end of MD3, and the point of them is that they move.

It morphs, and it morphs between any two of them. Change shape and the outline travels; give it hover or press and it answers the pointer. There is no list of legal pairs, because the morph does not interpolate path data — every outline is walked into the same ring of 72 points, wound the same way and started at the same place, so a heart can become a pixel triangle as easily as a circle becomes a square. The content underneath never moves; only the hole it is seen through does.

Spin does not tilt the picture. spin turns the outline while the contents counter-rotate on the same clock, so a face inside a spinning cookie stays upright — the MD3 loading indicator's trick, and the reason spin with cycle reads as one moving object rather than a rotating image.

Everything respects prefers-reduced-motion: morphs land immediately, spinning and cycling stop. Shapes are clipped in objectBoundingBox space, so a square box keeps them true and a rectangle stretches them on purpose.

import { Shape } from 'omaris'
<Shape shape="cookie-9" hover="clover-4" src={user.photo} alt={user.name} size="5rem" /><Shape cycle={['cookie-9', 'clover-4', 'pill']} spin class="size-10 bg-primary" />

Parts

Every element this renders is reachable from outside. Pass Tailwind for one part as classes={{ part: '…' }}; class covers the root and is merged last, so it always wins.

root
The clipped box. Sizing, colour and elevation live here, and so does the spin — the outline is a clip-path on this element, so turning it turns the shape.
content
Everything inside, on its own layer so it can be turned back the other way while the shape spins. Also the reason a child can be absolute without escaping the clip.
image
No description in the source yet.

Props

shape

Defaults to 'circle'

ShapeName

The outline. Changing it morphs, rather than cutting.

hover
ShapeName

Morph to this while the pointer is over it, or focus is inside.

press
ShapeName

Morph to this while it is being pressed.

cycle
ShapeName[]

Morph through these forever — a loader, or an idle flourish.

interval

Defaults to 1400

number

How long each shape in cycle is held, in ms.

duration

Defaults to 500

number

How long one morph takes, in ms. 0 cuts instead.

spin

Defaults to false

boolean | number

Turn the outline while the contents stay upright. Seconds per turn, or true for 12.

size

Defaults to '6rem'

string | number

Width and height, as a CSS length. Numbers are pixels.

src
string

A picture to clip, for the common case. Children work too.

alt
string
class
string
classes
ShapeClasses

Per-part Tailwind overrides. class still covers the root.

children
Snippet