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.

Horizontal.astro — full source
---
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>
1 data-shape identity — the root element names itself, no class names
2 Primitive composition — Avatar and Timestamp are reused, not redefined
3 @scope containment — CSS targets elements by type, never leaks
4 Theme token readsvar(--surface), var(--on-surface) — colours come from outside
Rendered output
AW

Alan White

Admin
HTML output — zero class names
<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.

Interface — UserProfile
export interface UserProfile {
  name: string;
  initials: string;
  role: string;
  joinedAt?: string;
}
Horizontal
AW

Alan White

Admin
Dashboard sidebar
Vertical
AW

Alan White

Admin
Profile page
Inline

Written by AW Alan White in the design channel.

Inline prose

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.

Shapes own
  • Layout & display mode
  • Spacing & gap
  • Padding & border-radius
  • Primitive composition
  • Slot definitions
  • Element structure & order
Shapes don't own
  • 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.

theme="light"
AW

Alan White

Admin
theme="dark"
AW

Alan White

Admin
Usage — passing the theme prop
<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

File structure
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
// 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
// Consuming page
import * as UserProfile from '../intershapes/profile/shapes';

<UserProfile.Horizontal data={user} />
<UserProfile.Vertical   data={user} />
<UserProfile.Inline     data={user} />

Next steps