Skip to content
edmi-ui

Feature Row

Numbered feature row on a card with a trailing icon; expands when given content.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
1.2Index identity
1.3Vault deployment
1.2Index identity
1.3Vault deployment
1.2 Index identity
1.3 Vault deployment
Terminal window
npx shadcn@latest add @edmi-ui/feature-row
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/feature-row
Terminal window
yarn dlx shadcn@latest add @edmi-ui/feature-row
Terminal window
bunx shadcn@latest add @edmi-ui/feature-row

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/feature-row
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/feature-row
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/feature-row
Terminal window
bunx shadcn-vue@latest add @edmi-ui/feature-row

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/feature-row.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/feature-row.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/feature-row.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/feature-row.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { FeatureRow } from "@/components/feature-row/feature-row";
<div className="flex w-[420px] max-w-full flex-col gap-2">
<FeatureRow index="1.1" title="Thesis and weights">
Describe the thesis, pick up to 10 assets and set their weights.
</FeatureRow>
<FeatureRow index="1.2" title="Index identity" />
<FeatureRow index="1.3" title="Vault deployment" />
</div>
<script setup lang="ts">
import { FeatureRow } from "@/components/ui/feature-row";
</script>
<template>
<div class="flex w-[420px] max-w-full flex-col gap-2">
<FeatureRow index="1.1" title="Thesis and weights">
Describe the thesis, pick up to 10 assets and set their weights.
</FeatureRow>
<FeatureRow index="1.2" title="Index identity" />
<FeatureRow index="1.3" title="Vault deployment" />
</div>
</template>
<script lang="ts">
import { FeatureRow } from "$lib/components/ui/feature-row";
</script>
<div class="flex w-[420px] max-w-full flex-col gap-2">
<FeatureRow index="1.1" title="Thesis and weights">
Describe the thesis, pick up to 10 assets and set their weights.
</FeatureRow>
<FeatureRow index="1.2" title="Index identity" />
<FeatureRow index="1.3" title="Vault deployment" />
</div>
registry/blocks/feature-row/feature-row.tsx
"use client";
import { cn } from "cn";
import type * as React from "react";
import { PlusIcon } from "@phosphor-icons/react";
import { Card } from "@/registry/edmi/ui/card";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/registry/edmi/ui/collapsible";
import type { Elevation } from "@/registry/edmi/ui/elevation";
type FeatureRowProps = Omit<React.ComponentProps<typeof Card>, "title"> & {
/** Mono number, e.g. "1.1". */
index: React.ReactNode;
title: React.ReactNode;
/** Trailing icon element; defaults to a plus. */
icon?: React.ReactNode;
/** When set the row expands to show this content. */
children?: React.ReactNode;
/** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2. */
elevation?: Elevation;
};
const plus = (
<PlusIcon
className="size-4"
/>
);
function FeatureRow({
className,
index,
title,
icon = plus,
children,
elevation,
...props
}: FeatureRowProps) {
const head = (
<>
<span className="font-mono text-xs text-muted-foreground">{index}</span>
<span className="flex-1 text-left text-base font-medium">{title}</span>
<span className="text-muted-foreground transition-transform group-data-[panel-open]/feature-row:rotate-45">
{icon}
</span>
</>
);
if (!children) {
return (
<Card
data-slot="feature-row"
elevation={elevation}
className={cn("h-14 flex-row items-center gap-4 px-5 py-0", className)}
{...props}
>
{head}
</Card>
);
}
return (
<Card
data-slot="feature-row"
elevation={elevation}
className={cn("gap-0 py-0", className)}
{...props}
>
<Collapsible>
<CollapsibleTrigger className="group/feature-row flex h-14 w-full cursor-pointer items-center gap-4 px-5 outline-none focus-visible:bg-accent">
{head}
</CollapsibleTrigger>
<CollapsibleContent className="px-5 pb-4 pl-[52px] text-sm text-muted-foreground">
{children}
</CollapsibleContent>
</Collapsible>
</Card>
);
}
export { FeatureRow };
registry/ui/feature-row/FeatureRow.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { Plus } from '@lucide/vue'
import { Card } from '@/registry/edmi/ui/card'
import type { Elevation } from '@/registry/edmi/ui/elevation'
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/registry/edmi/ui/collapsible'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{
/** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2 */
elevation?: Elevation
class?: HTMLAttributes['class']
/** Mono number, e.g. "1.1". */
index?: string | number
title?: string
}>()
// Slots `index` and `title` (string props are the fallback). Default slot = expandable content; slot `icon` = trailing icon (defaults to a plus).
</script>
<template>
<Card :elevation="elevation"
v-if="!$slots.default"
data-slot="feature-row"
:class="cn('h-14 flex-row items-center gap-4 px-5 py-0', props.class)"
>
<span class="font-mono text-xs text-muted-foreground"><slot name="index">{{ index }}</slot></span>
<span class="flex-1 text-left text-base font-medium"><slot name="title">{{ title }}</slot></span>
<span class="text-muted-foreground">
<slot name="icon"><Plus class="size-4" /></slot>
</span>
</Card>
<Card :elevation="elevation" v-else data-slot="feature-row" :class="cn('gap-0 py-0', props.class)">
<Collapsible class="group/feature-row">
<CollapsibleTrigger
class="flex h-14 w-full cursor-pointer items-center gap-4 px-5 outline-none focus-visible:bg-accent"
>
<span class="font-mono text-xs text-muted-foreground"><slot name="index">{{ index }}</slot></span>
<span class="flex-1 text-left text-base font-medium"><slot name="title">{{ title }}</slot></span>
<span class="text-muted-foreground transition-transform group-data-[state=open]/feature-row:rotate-45">
<slot name="icon"><Plus class="size-4" /></slot>
</span>
</CollapsibleTrigger>
<CollapsibleContent class="px-5 pb-4 pl-[52px] text-sm text-muted-foreground">
<slot />
</CollapsibleContent>
</Collapsible>
</Card>
</template>
registry/ui/feature-row/index.ts
export { default as FeatureRow } from "./FeatureRow.vue";
src/lib/registry/ui/feature-row/feature-row.svelte
<script lang="ts">
import { cn } from "$lib/utils.js";
import { Card } from "$lib/registry/ui/card/index.js";
import type { Elevation } from "$lib/registry/ui/elevation/index.js";
import * as Collapsible from "$lib/registry/ui/collapsible/index.js";
import PlusIcon from "phosphor-svelte/lib/Plus";
import type { ComponentProps, Snippet } from "svelte";
let {
ref = $bindable(null),
class: className,
index,
title,
icon,
children,
elevation = "auto",
...restProps
}: Omit<ComponentProps<typeof Card>, "title" | "children"> & {
/** Mono number, e.g. "1.1". */
index: string | number | Snippet;
title: string | Snippet;
/** Trailing icon; defaults to a plus. */
icon?: Snippet;
/** When set the row expands to show this content. */
children?: Snippet;
/** ✦ depth of the card: sunken -1, flat 0, raised +1, floating +2. */
elevation?: Elevation;
} = $props();
</script>
{#snippet plus()}
<PlusIcon
class="size-4"
/>
{/snippet}
{#if !children}
<Card bind:ref {elevation} data-slot="feature-row" class={cn("h-14 flex-row items-center gap-4 px-5 py-0", className)} {...restProps}>
<span class="font-mono text-xs text-muted-foreground">{#if typeof index === "function"}{@render index()}{:else}{index}{/if}</span>
<span class="flex-1 text-left text-base font-medium">{#if typeof title === "function"}{@render title()}{:else}{title}{/if}</span>
<span class="text-muted-foreground">{@render (icon ?? plus)()}</span>
</Card>
{:else}
<Card bind:ref {elevation} data-slot="feature-row" class={cn("gap-0 py-0", className)} {...restProps}>
<Collapsible.Root class="group/feature-row">
<Collapsible.Trigger
class="flex h-14 w-full cursor-pointer items-center gap-4 px-5 outline-none focus-visible:bg-accent"
>
<span class="font-mono text-xs text-muted-foreground">{#if typeof index === "function"}{@render index()}{:else}{index}{/if}</span>
<span class="flex-1 text-left text-base font-medium">{#if typeof title === "function"}{@render title()}{:else}{title}{/if}</span>
<span class="text-muted-foreground transition-transform group-data-[state=open]/feature-row:rotate-45">
{@render (icon ?? plus)()}
</span>
</Collapsible.Trigger>
<Collapsible.Content class="px-5 pb-4 pl-[52px] text-sm text-muted-foreground">
{@render children()}
</Collapsible.Content>
</Collapsible.Root>
</Card>
{/if}
src/lib/registry/ui/feature-row/index.ts
import Root from "./feature-row.svelte";
export {
Root,
//
Root as FeatureRow,
};

See the Elevation guide for the levels, roles and layered mode.

Flat by default. elevation is forwarded to the card: sunken (-1) a soft inset well, raised (+1) the bevel, floating (+2) the bevel plus one soft drop. In a layered ElevationProvider the card resolves to its role default (raised).

StackBlitz ↗StackBlitz ↗StackBlitz ↗

Flat (0)

1.2Index identity
1.3Vault deployment

Raised (+1)

1.2Index identity
1.3Vault deployment

Floating (+2)

1.2Index identity
1.3Vault deployment

Flat (0)

1.2Index identity
1.3Vault deployment

Raised (+1)

1.2Index identity
1.3Vault deployment

Floating (+2)

1.2Index identity
1.3Vault deployment

Flat (0)

1.2 Index identity
1.3 Vault deployment

Raised (+1)

1.2 Index identity
1.3 Vault deployment

Floating (+2)

1.2 Index identity
1.3 Vault deployment
Part Prop Type Default
FeatureRow index React ReactNode; Vue slot index (prop string | number as fallback); Svelte string | number | Snippet required
title React ReactNode; Vue slot title (string prop as fallback); Svelte string | Snippet required
icon React ReactNode; Vue slot icon; Svelte icon snippet plus
children Expandable content (Vue default slot, Svelte children). With content the 56px row becomes a Collapsible and the plus rotates to a cross none
elevation ✦ "auto" | "sunken" | "flat" | "raised" | "floating" "auto"
  • Whole block is an Edmi addition (DESIGN §5 Patterns, board 12). Composed from: card, collapsible.
  • Install: shadcn add @edmi-ui/feature-row.