Skip to content
edmi-ui

Allocation Bar

Proportional weights bar with a legend of labels and mono percentages.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
  • AAPLx40%
  • NVDAx30%
  • TSLAx20%
  • SPACEX-pre10%
  • AAPLx 40%
  • NVDAx 30%
  • TSLAx 20%
  • SPACEX-pre 10%
  • AAPLx 40%
  • NVDAx 30%
  • TSLAx 20%
  • SPACEX-pre 10%
Terminal window
npx shadcn@latest add @edmi-ui/allocation-bar
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/allocation-bar
Terminal window
yarn dlx shadcn@latest add @edmi-ui/allocation-bar
Terminal window
bunx shadcn@latest add @edmi-ui/allocation-bar

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/allocation-bar
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/allocation-bar
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/allocation-bar
Terminal window
bunx shadcn-vue@latest add @edmi-ui/allocation-bar

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/allocation-bar.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/allocation-bar.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/allocation-bar.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/allocation-bar.json

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

import { AllocationBar } from "@/components/allocation-bar/allocation-bar";
<AllocationBar
className="max-w-md"
segments={[
{ label: "AAPLx", value: 40 },
{ label: "NVDAx", value: 30 },
{ label: "TSLAx", value: 20 },
{ label: "SPACEX-pre", value: 10 },
]}
/>
<script setup lang="ts">
import { AllocationBar } from "@/components/ui/allocation-bar";
</script>
<template>
<AllocationBar class="max-w-md" />
</template>
<script lang="ts">
import { AllocationBar } from "$lib/components/ui/allocation-bar";
</script>
<AllocationBar class="max-w-md" />
registry/blocks/allocation-bar/allocation-bar.tsx
import { cn } from "cn";
import type * as React from "react";
type AllocationSegment = {
label: string;
/** Weight in percent (any positive number; bars are proportional). */
value: number;
/** Any CSS color; defaults cycle `--chart-1…5`. */
color?: string;
};
// Weights bar with a legend of label + mono percentage.
function AllocationBar({
className,
segments,
showLegend = true,
size = "default",
...props
}: Omit<React.ComponentProps<"div">, "children"> & {
segments: AllocationSegment[];
showLegend?: boolean;
/** ✦ `sm`: slimmer bar and a compact mono legend (cards). */
size?: "default" | "sm";
}) {
const sm = size === "sm";
const colored = segments.map((s, i) => ({
...s,
color: s.color ?? `var(--chart-${(i % 5) + 1})`,
}));
return (
<div
data-slot="allocation-bar"
className={cn("w-full", className)}
{...props}
>
<div
className="flex gap-[3px]"
role="img"
aria-label={colored.map((s) => `${s.label} ${s.value}%`).join(", ")}
>
{colored.map((s) => (
<span
key={s.label}
className={sm ? "h-2 rounded-[3px]" : "h-2.5 rounded-full"}
style={{ flex: s.value, background: s.color }}
/>
))}
</div>
{showLegend ? (
<ul
className={cn(
"flex flex-wrap items-center gap-x-4 gap-y-1.5",
sm ? "mt-3 text-xs text-foreground-2" : "mt-2.5 text-[12.5px]",
)}
>
{colored.map((s) => (
<li
key={s.label}
className={cn("flex items-center gap-1.5", sm && "font-mono")}
>
<span
className={
sm ? "size-2 rounded-[2px]" : "size-[9px] rounded-[3px]"
}
style={{ background: s.color }}
/>
{s.label}
<b
className={cn(
"font-mono",
sm ? "font-semibold text-foreground" : "font-medium",
)}
>
{s.value}%
</b>
</li>
))}
</ul>
) : null}
</div>
);
}
export type { AllocationSegment };
export { AllocationBar };
registry/ui/allocation-bar/AllocationBar.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import type { AllocationSegment } from '.'
import { computed } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
const props = withDefaults(defineProps<{
segments: AllocationSegment[]
showLegend?: boolean
/** sm: slimmer bar and a compact mono legend (cards). */
size?: 'default' | 'sm'
class?: HTMLAttributes['class']
}>(), {
showLegend: true,
size: 'default',
})
const colored = computed(() =>
props.segments.map((s, i) => ({ ...s, color: s.color ?? `var(--chart-${(i % 5) + 1})` })),
)
</script>
<template>
<!-- Weights bar with a legend of label + mono percentage. -->
<div data-slot="allocation-bar" :class="cn('w-full', props.class)">
<div
class="flex gap-[3px]"
role="img"
:aria-label="colored.map(s => `${s.label} ${s.value}%`).join(', ')"
>
<span
v-for="s in colored"
:key="s.label"
:class="size === 'sm' ? 'h-2 rounded-[3px]' : 'h-2.5 rounded-full'"
:style="{ flex: s.value, background: s.color }"
/>
</div>
<ul
v-if="showLegend"
:class="cn('flex flex-wrap items-center gap-x-4 gap-y-1.5', size === 'sm' ? 'mt-3 text-xs text-foreground-2' : 'mt-2.5 text-[12.5px]')"
>
<li v-for="s in colored" :key="s.label" :class="cn('flex items-center gap-1.5', size === 'sm' && 'font-mono')">
<span :class="size === 'sm' ? 'size-2 rounded-[2px]' : 'size-[9px] rounded-[3px]'" :style="{ background: s.color }" />
{{ s.label }}
<b :class="cn('font-mono', size === 'sm' ? 'font-semibold text-foreground' : 'font-medium')">{{ s.value }}%</b>
</li>
</ul>
</div>
</template>
registry/ui/allocation-bar/index.ts
export type AllocationSegment = {
label: string;
/** Weight in percent (any positive number; bars are proportional). */
value: number;
/** Any CSS color; defaults cycle `--chart-1…5`. */
color?: string;
};
export { default as AllocationBar } from "./AllocationBar.vue";
src/lib/registry/ui/allocation-bar/allocation-bar.svelte
<script lang="ts" module>
export type AllocationSegment = {
label: string;
/** Weight in percent (any positive number; bars are proportional). */
value: number;
/** Any CSS color; defaults cycle `--chart-1…5`. */
color?: string;
};
</script>
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
segments,
showLegend = true,
size = "default",
...restProps
}: WithElementRef<Omit<HTMLAttributes<HTMLDivElement>, "children">> & {
segments: AllocationSegment[];
showLegend?: boolean;
/** ✦ `sm`: slimmer bar and a compact mono legend (cards). */
size?: "default" | "sm";
} = $props();
const colored = $derived(
segments.map((s, i) => ({ ...s, color: s.color ?? `var(--chart-${(i % 5) + 1})` }))
);
</script>
<!-- Weights bar with a legend of label + mono percentage. -->
<div bind:this={ref} data-slot="allocation-bar" class={cn("w-full", className)} {...restProps}>
<div class="flex gap-[3px]" role="img" aria-label={colored.map((s) => `${s.label} ${s.value}%`).join(", ")}>
{#each colored as s (s.label)}
<span class={size === "sm" ? "h-2 rounded-[3px]" : "h-2.5 rounded-full"} style:flex={s.value} style:background={s.color}></span>
{/each}
</div>
{#if showLegend}
<ul class={cn("flex flex-wrap items-center gap-x-4 gap-y-1.5", size === "sm" ? "mt-3 text-xs text-foreground-2" : "mt-2.5 text-[12.5px]")}>
{#each colored as s (s.label)}
<li class={cn("flex items-center gap-1.5", size === "sm" && "font-mono")}>
<span class={size === "sm" ? "size-2 rounded-[2px]" : "size-[9px] rounded-[3px]"} style:background={s.color}></span>
{s.label}
<b class={cn("font-mono", size === "sm" ? "font-semibold text-foreground" : "font-medium")}>{s.value}%</b>
</li>
{/each}
</ul>
{/if}
</div>
src/lib/registry/ui/allocation-bar/index.ts
export {
type AllocationSegment,
default as AllocationBar,
} from "./allocation-bar.svelte";

Flat only: the bar and legend render no surface, so there is no elevation prop and no elevation demo. See the Elevation guide.

Part Prop Type Default
AllocationBar segments { label, value, color? }[] required
showLegend boolean true

Colors default to --chart-1…5 in order.

  • Bars are proportional to value; values need not sum to 100.