Mastering Astryx / lesson 10 of 13

Application Toolbars with Selector & IconButton

Build the top application toolbar with a sprint selector, action icon buttons, and the theme and language toggles.

Play
Transcript

[00:00] So our Kanban application is coming along really nicely. And what we’re going to do in this video is we are going to address the kind of dumb looking application toolbar we have here and jump over to our app and we are going to create a new component called board toolbar. And I’m going to drop a bunch of stuff in here and I’m going to come right back to it. Cause I want to jump over to messages. We’re going to create a new localization string for add task in both English and French. So let’s jump back to our board toolbar.

[00:36] So we’re bringing in just the kitchen sink when it comes to components available to us in the Astryx design system. So we’ve got badges and headings. We’ve got an actual toolbar component, selector, divider, icon buttons. A lot of these we haven’t used use translator. We have used, and that’s why I needed to bring in or create that new, um, key for add tasks. Cause we’re going to have a little button that says add task. Um, one thing I’ll point out is there’s a language icon. I don’t know what that is, but I used it cause it was there. So my language switcher is now going to have this icon. I don’t know what it is.

[01:12] Uh, okay. So what are we looking at? We’ve got our board tool props, which is total tasks, the sprint and sprint on change, which isn’t, uh, actually going to function. Um, we are inside of a toolbar, which works a lot like everything else. We’ve got a label, we’ve got a gap, we’ve got this start content, which can be a render props here. So we’ve got a heading and a badge, nothing too complicated there. Uh, and then we’ve got an end content. Uh, so this is, you know, left and right in this toolbar, we’ve got a divider, we’ve got an H stack. So a horizontal stack with a button for, uh, sorting, filtering, searching, none of these actually work. I just wanted to show off the icon button and the render prop for rendering an icon in there. And we’re using icons from that hero icon library. We’ve got another divider, uh, orientation of vertical. We’re using some X style here. Uh, and then we’ve tweaked our, or we’re tweaking our, um, current light mode guy, uh, to an icon button. So all the, all the code’s still there. It works just the way it did more or less, uh, except we’ve got a sun icon and a moon icon. Uh, and then we’ve got an icon for the, uh, switching the language, which I mentioned is using this language icon. You’ll see it in a second. And there’s button for adding a task. And again, a lot of these don’t function other than the, um, uh, uh, light and dark mode and the language mode. But I just wanted to kind of, like I said, throw a whole bunch of stuff in here and look at all these really awesome components that are available to us. And they just work really well together. All of this was super simple. I didn’t really have to think about it. There’s a CLI that lets you look up any of these components very quickly. Uh, they’re all named really well. It’s just, it’s just very easy to get around in this design, uh, system. So we’re going to save that.

[03:06] We’re going to jump over to our Kanban board and we’ve actually got to make a lot of changes here. Uh, one thing we’re going to end up needing is use state from react. We don’t need this card anymore. We actually didn’t need it before. We don’t need this heading, this button, the translator, or the preferences because all of that is going to live in that toolbar now. So our Kanban board is, is really, you know, getting cleaned up here. Uh, that means we don’t need this guy. We don’t need this guy. And I think everything inside this layout header is where our board toolbar is going to go. Let me just format all that. And we do need to import our board toolbar. And I’m going to set up a mock sprint here. Cool. I think all of this is looking good. I’m going to reload this guy and look at our awesome toolbar.

[04:07] So we can switch between light and dark. We can change our language with whatever that language icon is. Uh, we’ve got a dropdown here. It’s not actually doing anything, but it’s kind of cool. We can see it and we have moved a lot of, uh, that functionality out of our Kanban board into this board toolbar. So we’re getting much more modular here. This is, this is really starting to come together very well. So, um, I think the next thing is really like, let’s get this frigging drag and drop working. Uh, so we’re going to do that next.

The header is still inline in KanbanBoard — title, task counter, and the two toggle buttons. In this lesson we consolidate everything into BoardToolbar, the top application bar: board title, total badge, sprint selector, action buttons, and the theme + language toggles.

Files this lesson: src/app/components/BoardToolbar.tsx (new), src/app/messages.ts (extended), src/app/components/KanbanBoard.tsx (rewritten). page.tsx stays untouched.

Astryx this lesson: Toolbar, Heading, Badge, Selector, Divider (vertical), IconButton, Icon, Button. Concepts: application action bars, single-select dropdowns, divider separation, and primary action buttons.

Toolbar Components

Implementation: BoardToolbar.tsx

First extend the catalogs with the “Add task” string so it localizes:

src/app/messages.ts
export const en: Catalog = {
'@app.board.title': { defaultMessage: 'Sprint Board' },
'@app.switchLanguage': { defaultMessage: 'En français' },
'@app.tasks': { defaultMessage: 'tasks' },
'@app.addTask': { defaultMessage: 'Add task' },
};
export const fr: Catalog = {
'@app.board.title': { defaultMessage: 'Tableau de sprint' },
'@app.switchLanguage': { defaultMessage: 'In English' },
'@app.tasks': { defaultMessage: 'tâches' },
'@app.addTask': { defaultMessage: 'Ajouter une tâche' },
};

Create src/app/components/BoardToolbar.tsx:

src/app/components/BoardToolbar.tsx
'use client';
import { Toolbar } from '@astryxdesign/core/Toolbar';
import { Heading } from '@astryxdesign/core/Text';
import { Badge } from '@astryxdesign/core/Badge';
import { Selector } from '@astryxdesign/core/Selector';
import { Divider } from '@astryxdesign/core/Divider';
import { IconButton } from '@astryxdesign/core/IconButton';
import { Button } from '@astryxdesign/core/Button';
import { Icon } from '@astryxdesign/core/Icon';
import { HStack } from '@astryxdesign/core/Layout';
import { useTranslator } from '@astryxdesign/core/i18n';
import {
ArrowsUpDownIcon,
FunnelIcon,
MagnifyingGlassIcon,
MoonIcon,
SunIcon,
LanguageIcon,
PlusIcon,
} from '@heroicons/react/24/outline';
import { styles } from '../styles';
import { usePreferences } from '../providers';
interface BoardToolbarProps {
totalTasks: number;
sprint: string;
onSprintChange: (sprint: string) => void;
}
export function BoardToolbar({
totalTasks,
sprint,
onSprintChange,
}: BoardToolbarProps) {
const t = useTranslator();
const { mode, setMode, locale, setLocale } = usePreferences();
return (
<Toolbar
label="Board actions"
gap={2}
startContent={
<>
<Heading level={3}>{t('@app.board.title')}</Heading>
<Badge label={totalTasks} variant="neutral" />
</>
}
endContent={
<HStack gap={2}>
<Selector
label="Sprint"
width={200}
isLabelHidden
value={sprint}
onChange={onSprintChange}
options={[
{ value: '003', label: 'Sprint 003' },
{ value: '002', label: 'Sprint 002' },
{ value: '001', label: 'Sprint 001' },
]}
/>
<Divider
variant="strong"
orientation="vertical"
xstyle={styles.toolbarDivider}
/>
<HStack gap={1} vAlign="center">
<IconButton
icon={<Icon icon={ArrowsUpDownIcon} size="sm" />}
label="Sort"
/>
<IconButton
icon={<Icon icon={FunnelIcon} size="sm" />}
label="Filter"
/>
<IconButton
icon={<Icon icon={MagnifyingGlassIcon} size="sm" />}
label="Search"
/>
</HStack>
<Divider
variant="strong"
orientation="vertical"
xstyle={styles.toolbarDivider}
/>
{/* Dark / light toggle — cycles system → light → dark → system */}
<IconButton
icon={<Icon icon={mode === 'dark' ? SunIcon : MoonIcon} size="sm" />}
label={mode === 'dark' ? 'Light mode' : 'Dark mode'}
onClick={() =>
setMode(
mode === 'system'
? 'light'
: mode === 'light'
? 'dark'
: 'system',
)
}
/>
{/* Language toggle */}
<IconButton
icon={<Icon icon={LanguageIcon} size="sm" />}
label={t('@app.switchLanguage')}
onClick={() => setLocale(locale === 'en' ? 'fr' : 'en')}
/>
<Button
label={t('@app.addTask')}
variant="primary"
icon={<Icon icon={PlusIcon} size="sm" />}
/>
</HStack>
}
/>
);
}

The two toggles you built in lessons 3 and 5 now live in the toolbar as icon buttons, reading the same usePreferences() context — the app-level controls are all in one place. The moon/sun icon reflects the current mode, and the language button keeps its localized label.

Using It: KanbanBoard.tsx

Rewrite src/app/components/KanbanBoard.tsx to lift sprint into state and hand the whole header to BoardToolbar:

src/app/components/KanbanBoard.tsx
'use client';
import { useState } from 'react';
import { Section } from '@astryxdesign/core/Section';
import {
Layout,
LayoutHeader,
LayoutContent,
HStack,
VStack,
} from '@astryxdesign/core/Layout';
import { Heading, Text } from '@astryxdesign/core/Text';
import { Button } from '@astryxdesign/core/Button';
import { useTranslator } from '@astryxdesign/core/i18n';
import { usePreferences } from '../providers';
import { styles } from '../styles';
import { BoardCard } from './BoardCard';
import { BoardColumn } from './BoardColumn';
import { BoardToolbar } from './BoardToolbar';
import { COLUMNS, INITIAL_ITEMS } from '../data';
import type { ColumnId } from '../types';
export default function KanbanBoard() {
const t = useTranslator();
const { mode, setMode, locale, setLocale } = usePreferences();
const [sprint, setSprint] = useState('003');
// Temporary — real moves arrive with the drag hook in lesson 11.
const moveItem = (id: string, to: ColumnId) => {
alert(`Move task ${id} to ${to}`);
};
return (
<Section height="100dvh">
<Layout
height="fill"
header={
<LayoutHeader hasDivider padding={4}>
<HStack hAlign="between" vAlign="center">
<Heading level={3}>{t('@app.board.title')}</Heading>
<HStack gap={2} vAlign="center">
<Text type="supporting" color="secondary" hasTabularNumbers>
{INITIAL_ITEMS.length} {t('@app.tasks')}
</Text>
<Button
label={t('@app.switchLanguage')}
variant="secondary"
onClick={() => setLocale(locale === 'en' ? 'fr' : 'en')}
/>
<Button
label={
mode === 'system'
? 'Light mode'
: mode === 'light'
? 'Dark mode'
: 'System mode'
}
variant="secondary"
onClick={() =>
setMode(
mode === 'system'
? 'light'
: mode === 'light'
? 'dark'
: 'system',
)
}
/>
</HStack>
</HStack>
<BoardToolbar
totalTasks={INITIAL_ITEMS.length}
sprint={sprint}
onSprintChange={setSprint}
/>
</LayoutHeader>
}
content={
<LayoutContent padding={0}>
<HStack gap={4} xstyle={styles.boardColumns}>
{COLUMNS.map(col => {
const items = INITIAL_ITEMS.filter(it => it.column === col.id);
return (
<BoardColumn
key={col.id}
meta={col}
count={items.length}
contentRef={() => {}}
>
{/* null children trigger BoardColumn's EmptyState fallback */}
{items.length > 0 ? (
<VStack gap={2}>
{items.map(item => (
<BoardCard
key={item.id}
item={item}
cardRef={() => {}}
onPointerDown={() => {}}
onMove={moveItem}
/>
))}
</VStack>
) : null}
</BoardColumn>
);
})}
</HStack>
</LayoutContent>
}
/>
</Section>
);
}

KanbanBoard now owns the sprint state and passes it down; the header, title, counters, and toggles all moved into BoardToolbar. Note the header no longer needs Heading/Text/Button imports — the toolbar owns that rendering now.

See It in Action

Run npm run dev. The header is now a single Toolbar: title + total badge on the left; sprint selector, action icons, theme toggle, language toggle, and the Add task primary button on the right. Change the sprint — the selector reflects it. Flip the theme (moon/sun icon swaps) and the language — both still work, now as compact icon buttons in the toolbar.

What You Built

In the next lesson, we will extract the drag-and-drop interaction engine into a custom hook.


Share this post on:

Previous
Column Shells with StatusDot, Tooltip & EmptyState
Next
Drag-and-Drop Interactions & Astryx Ref Forwarding