Shapes
A shape is a fixed, immutable HTML structure that visually represents an interface. It names itself with data-shape, contains no class names on children, styles itself via @scope, and reads colour from theme tokens. Multiple shapes can implement the same interface — swap the shape, the data stays the same.
30 shapes. 22 interfaces. 4 themes. 120 visual outputs.
Note: The examples on this page use Astro, @scope CSS, and data-theme attributes — but this is just one implementation. The Intershapes model is conceptual: it separates structure from colour regardless of framework or styling technology.
Anatomy of a shape
The profile-horizontal shape dissected. Every shape follows this same structure — frontmatter types, semantic template, scoped CSS.
---
import type { UserProfile } from '../interface';
import type { ThemeName } from '../../_types';
import Avatar from '../../_primitives/Avatar.astro';
import Timestamp from '../../_primitives/Timestamp.astro';
interface Props {
data: UserProfile;
theme?: ThemeName;
}
const { data, theme } = Astro.props;
---
<article data-shape="profile-horizontal" data-theme={theme}>
<Avatar initials={data.initials} size="md" />
<div>
<h3>{data.name}</h3>
<span>{data.role}</span>
{data.joinedAt && <Timestamp text={data.joinedAt} size="sm" />}
</div>
</article>
<style is:global>
@scope ([data-shape="profile-horizontal"]) {
:scope {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.875rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
}
h3 {
font-size: 0.875rem;
font-weight: 600;
color: var(--on-surface);
margin: 0;
}
span {
font-size: 0.75rem;
color: var(--on-surface-muted);
display: block;
}
}
</style> var(--surface), var(--on-surface) — colours come from outside <article data-shape="profile-horizontal">
<span data-primitive="avatar" data-size="md">AW</span>
<div>
<h3>Alan White</h3>
<span>Admin</span>
<time data-primitive="timestamp" data-size="sm">January 2024</time>
</div>
</article> One interface, many shapes
The UserProfile interface defines the data contract. Three shapes implement it — each a completely different layout, but all consuming the same props.
export interface UserProfile {
name: string;
initials: string;
role: string;
joinedAt?: string;
} Alan White
AdminAlan White
AdminWritten by AW Alan White in the design channel.
Same data. Different layouts. Swap the shape, nothing else changes. The interface guarantees the contract. Each shape is a complete visual expression of that contract — not a variant prop, not a conditional branch.
What shapes own
Shapes and themes have cleanly separated responsibilities. This separation is what makes the system composable — you can change either axis independently.
- Layout & display mode
- Spacing & gap
- Padding & border-radius
- Primitive composition
- Slot definitions
- Element structure & order
- Colours (from theme tokens)
- State management
- Data fetching
- Class names on children
- Theme values
- Business logic
Same shape, different theme
The HTML is identical in both cases. Only colour changes — the theme provides new token values, the shape reads them. No conditional logic, no variant props.
Alan White
AdminAlan White
Admin<UserProfile.Horizontal data={user} theme="dark" /> Two independent axes. Structure × Colour. 3 shapes × 4 themes = 12 visual outputs from 7 definitions. Neither axis knows about the other.
Conventions
Naming, file structure, and import patterns that keep the system predictable as it grows.
data-shape naming
Every shape names itself with a data-shape attribute following the pattern {entity}-{shapename}. This is the CSS scope boundary and the only identifier the shape needs.
profile-horizontal profile-vertical profile-inline comment-thread comment-compact notification-card notification-row Directory layout
src/intershapes/
profile/
interface.ts # UserProfile type
shapes/
Horizontal.astro # data-shape="profile-horizontal"
Vertical.astro # data-shape="profile-vertical"
Inline.astro # data-shape="profile-inline"
index.ts # barrel export
notification/
interface.ts # Notification type
shapes/
Card.astro # data-shape="notification-card"
Row.astro # data-shape="notification-row"
index.ts Barrel exports & namespace imports
Each shape directory has an index.ts that re-exports all shapes. Consuming pages import the namespace and access shapes by name.
// shapes/index.ts
export { default as Horizontal } from './Horizontal.astro';
export { default as Inline } from './Inline.astro';
export { default as Vertical } from './Vertical.astro'; // Consuming page
import * as UserProfile from '../intershapes/profile/shapes';
<UserProfile.Horizontal data={user} />
<UserProfile.Vertical data={user} />
<UserProfile.Inline data={user} />