A framework-free UI component library, built as native Web Components. 31 custom elements, each shipped as plain JavaScript with a matching usage guide. See "What's built" below for the full list and tag names.
Written for Phillip's teams and for the AI coding agents they build
screens with; both read the same rules. CLAUDE.md and AGENTS.md carry
the short form for agents.
Five rules that hold on every screen, before anything else in this file. A screen that breaks one is not finished, whatever else it gets right.
- One screen per preview. Each
<pds-preview>holds exactly one screen. A flow is several previews side by side, never one long scroll. - Two surfaces. The screen is
--pds-surface-1; a container on it is--pds-surface-2; that is the whole stack. Nothing on surface-2 gets a fill of its own. Rows separate with dividers or spacing, never with a third grey. - A card never holds a card. Containers (card, sheet, dialog) are placed by the screen, never inside each other. Rows and lists inside a card take the card's surface. Sub-items indent under their parent row; they never get a box of their own.
- Right chevron navigates, down chevron discloses.
chevron_rightat the end of a row means tapping it opens another screen.chevron_down, thenchevron_upwhile open, means the row expands in place. A row does one or the other, and a screen never gives one icon both meanings. - Money is code, space, two decimals.
SGD 10,000.00,USD 0.00. Never$orS$, never the code after the figure, never a bare amount. A price carries no code and the market's tick decimals (3.960on SGX); the code is stated once, in the label or the field suffix. A quantity is whole, with thousands separators. A percentage has two decimals and its sign, in brackets after the figure it qualifies.
A screen built with this library is a high-fidelity concept: it shows an idea in the real components so people can react to it. It does not settle requirements, it is not a URS, and it is not a build to hand to developers. Developers build from the design system and the agreed requirements; the concept shows what you mean. It does not cover backend logic, calculations, validation rules, error handling, flows that differ by account or product type, edge cases, or P3 and legacy constraints; those stay in the URS. Say this when someone asks what the screen is.
Read these first. They apply to every screen built with this library, in a new page or in an existing project, unless the prompt says otherwise.
-
Build from this library only. Every control on the screen is a
pds-*element from this repo. Never write your own button, input, tab, chip, switch, list, dialog or navigation, never style a raw<button>or<input>to look like one, and never copy patterns from another UI kit. Icons come fromicons/through<pds-icon name="…">; never paste SVG paths from another set or draw your own. If the library has no component or icon for what you need, say so in your answer and use the closest one. Do not invent it. -
The current POEMS app is not a reference. Do not reproduce its visuals or its screenshots. This library,
tokens/tokens.cssand the reference screens inexamples/are the only visual sources: use the tokens for every colour, never a hex value or a custom colour, and match the reference screen when one exists for the tab you are building (seeexamples/README.md, which says how current each one is). -
Bottom navigation only on root screens. Add
pds-navigation-bar(Home, Watchlist, Markets, Trade, Community, Me) only when the prompt names a root screen or asks for it. A detail screen, a form or a ticket has none. The status bar and the home line come with the preview frame on every screen. -
No logo in an app screen. POEMS app screens never carry the POEMS logo or wordmark in the header. The top app bar holds a title, navigation and actions only. The logo belongs to website pages, and only when the prompt says the screen is a website. Treat every screen as an app screen unless told it is a website.
-
Wrap the screen in
<pds-preview>. Importpreview/pds-preview.jsand put the whole screen inside<pds-preview>. It draws the phone-sized frame (375 px wide, 48 px corners) with the phone's status bar at the top and the home line at the bottom, and, above the frame and outside the screen, the reviewer's toggles: Light/Dark, Rounded/Square corners and Language. Do not build your own theme, corner or language controls, and never put such controls inside the screen. The frame paints the surface tokens, so the screen needs no page background of its own. Deliver the result as one live HTML page (an artifact), never as an image, a PDF or a zip: the toggles only work on the live page. A flow of several screens is one page with one<pds-preview>per screen, side by side, each with its own toggles. Seepreview/pds-preview.md. If a picture is asked for anyway (a screenshot, a PDF, an image for a document), render it withcorners="square", no toggles and no drawn status bar: square corners mark a static render. Rounded corners belong to the live page only, and a picture with rounded corners is not from this library. Font disclaimer: the library asks for Roboto but loads no web font; on a device without Roboto, text is shown in a fallback font and may not be 100% accurate. The preview shows a note when that is the case. -
Copy: short, complete, in three languages. Write every user-facing string as a complete sentence or label, as short as it can be. No fillers or fluff: no "please", "simply", "just", "in order to", no pleasantries. Give every string in English, modern Chinese (Simplified) and Japanese: text as
<span data-zh="…" data-ja="…">English</span>inside the component, attribute copy asdata-<attr>-zhanddata-<attr>-jaon the element (title,label,placeholder,supporting-text,error,message,action-label). The preview's Language toggle switches them. Seepreview/pds-preview.md. Disclaimer: Chinese and Japanese copy is not taken from POEMS translation files. Claude is tasked to suggest the shortest wording possible; translators review it before anything ships. -
End with what the reviewer must decide, nothing else. No build notes, no list of components used, no explanation of how you made the page. After the screen, add a line only when it applies, one line per item:
Substituted: <missing component> · used <what instead> Hand-built: <what> · <why the library had nothing for it> Logo in screen: <why, if it is a website page> Copy missing a language: <which strings>A clean screen ends with the screen.
These come from the design system's component documentation and apply to every screen, whatever the component.
- A financial value never truncates, rounds or breaks mid-number,
and carries its currency code in the form of non-negotiable 5
(
SGD 10,000.00). A name or a title may truncate as a last resort; a value never does. The column grows or scrolls instead. - A money action never reads OK, Yes or Submit. The label restates the action: "Place order", "Cancel order" / "Keep order". The amount shown is the amount that gets sent.
- Choice grammar. One of a few, all visible: a segmented button
(
pds-segmented-button) or a radio group. Several at once: chips or checkboxes. Takes effect the moment it is tapped: a switch. A chip is never a button, and a switch never sits behind Apply. - Text scaling. The app offers Regular, Large and Extra Large text (100, 115 and 130 %). At every size a label fits, then grows, then wraps, then a pair stacks. Nothing shrinks and nothing truncates.
- Confirmations and feedback. A decision that blocks is a dialog with a specific headline ("Cancel this order?"). A finished background action is a snackbar that says what happened. A sheet with a Cancel button cannot be dismissed by tapping outside; a disclaimer never can.
- Button width follows content, never a pixel value. A button hugs
its label by default. A button whose label toggles (Follow / Following)
reserves the width of its widest label (
reserve), so it never resizes on tap and a column of them lines up. Only the single screen-level action, and actions stacked to share one width, fill the container (block).
Each component's guide in src/<component>.md ends with the rules that
apply to it alone.
foundations/ holds the typography, spacing and radius tokens;
tokens/tokens.css loads them, so nothing extra to include. Hand-built
text takes a type class (pds-type-body-medium, pds-type-title-large,
pds-type-headline-small and so on), hand-built layout takes
--pds-space-* for every gap and --pds-radius-* for every corner.
Never a size, gap or radius off the scale; the smallest text is 11 px
and body copy is 14 or 16 px. The scales and the rules are in
foundations/README.md.
When a screen comes out wrong and one reply does not fix it, ask in Discussions (Q&A). Answers are marked when they settle the question, so the next person finds them. Put these four things in the post:
- The prompt, pasted as you typed it.
- The link to the result: the artifact or the page, not a picture of it.
- The lines Claude wrote under the screen (Substituted, Hand-built, Logo in screen, Copy missing a language), or "none".
- One screenshot of the part that is wrong, with the Light/Dark and Language toggles as you had them.
Say which Claude organisation you are in; links do not cross them.
One convention, so a name can be guessed:
- Tags:
pds-<thing>. A child of a container is the container's name plus-itemor the singular:pds-menu-item,pds-list-row,pds-navigation-item,pds-tab,pds-segment. - Attributes: lowercase, hyphenated (
label-position,action-label). Booleans are present or absent. Enumerated values are lowercase words (variant="secondary",tone="positive",corners="square"). - Events: plain lowercase names, no prefix:
change,select,dismiss,close,search,error.detailis an object:{ value },{ reason }. One exception: the preview firespreview-change, because the screen's ownchangeevents bubble through it. - Overlays open with
show()and close withclose(). The dialog also answers toshowModal(), the native name, which does the same. - Tokens:
--pds-plus the Figma variable path in kebab case (--pds-container-primary-1,--pds-transparency-outline-20); primitives--pds-<colour>-<step>; foundations--pds-type-<role>-<size>,--pds-space-<n>,--pds-radius-<name>; component knobs--pds-<component>-<property>. - Icons: snake_case, as Material names them (
arrow_left,visibility_off); the styled pairs are<name>_roundedand<name>_filled. - CSS parts: one lowercase word for the element (
button,input,dialog,scrim,segment). - Files: the tag name (
src/pds-text-field.jsand.md); folders in kebab case;examples/<screen-slug>/withreference.pngandindex.html. - Copy in three languages:
data-zh,data-ja,data-<attr>-zh,data-<attr>-ja. - Every guide reads in the same order: When to use, the component's own sections, Do, Don't, Rules from the design system documentation, Known limitations.
- No framework required.
<pds-button>and<pds-badge>work in a plain HTML page, or inside React, Vue, or anything else, because Custom Elements are a browser standard, not a framework feature. - No build step. Everything here is plain JavaScript with JSDoc type
comments, loaded with a native
<script type="module">. Building a page with these components doesn't require running a compiler. - Still typed, just not with TypeScript.
custom-elements.jsonis the Custom Elements Manifest standard: every attribute, its allowed values, its default, and every CSS custom property a component reads. A tool reading this repo gets the same exact-value guarantees a.d.tsfile would give it, without anyone having to runtsc.
- Read
custom-elements.jsonfirst: that's the ground truth for what each component accepts. - Read the matching
src/<component>.mdfor the when to use and do / don't guidance a props list can't express on its own. - Write plain HTML using the tags, e.g.
<pds-button size="large">Buy</pds-button>, importing./index.js(or the one component needed fromsrc/) and./tokens/tokens.css.
Use the stand-in below and say so in one "Substituted" line, so every screen makes the same choice:
| Need | Use instead |
|---|---|
| Tag or label next to a title ("Cash Plus", "Opted in") | A secondary text line under the title |
| Quick-action tile (icon over a caption) | pds-icon-button with a caption under it |
| Mascot or illustration | pds-icon name="ask_ai" for the AI entry point; otherwise leave it out |
| Stepper (minus, value, plus) | A pds-text-field between two pds-icon-buttons (minus, add) |
| Select or dropdown | A pds-text-field as the trigger slot of pds-menu, with chevron_down as its trailing icon |
| Bid/ask bar, quote strip | Not available: build it from pds-divider and text, say so in a Hand-built line |
| Date picker, slider, chart | Not available yet: a pds-text-field for the date or value, a static image for the chart, and say so |
Trade screens: Buy, price up and the bid side are green, Sell, price down
and the ask side are red. Use tone="positive" or tone="negative" on
pds-button, pds-chip and pds-segment, and the --pds-on-surface-text-positive and
--pds-on-surface-text-negative tokens for prices. The full V2 palette is
in tokens/tokens.css as --pds-<colour>-<step>; never type a hex value.
A label to the left of a field (the ticket layout) is
pds-text-field label-position="start".
brand/ holds the POEMS logo from the Foundations file, as SVG:
| File | Use |
|---|---|
brand/poems-logo.svg |
wordmark with the orange swoosh, 62 x 34, for light backgrounds |
brand/poems-logo-on-dark.svg |
same wordmark in white, for dark backgrounds |
brand/poems-mark.svg |
the square "p" mark, 34 x 34, for light backgrounds |
brand/poems-mark-on-dark.svg |
the "p" mark in white, for dark backgrounds |
Website pages only (see rule 1). Do not recolour, stretch or redraw them;
scale with width/height on an <img> and keep the aspect ratio.
icons/ holds the Phillip Design System V2 icon set: 135 SVG files,
24 x 24, listed in icons/icons.json. Use them through <pds-icon>:
<pds-icon name="search"></pds-icon>
<pds-icon name="notification" label="Notifications"></pds-icon>Ten icons have a rounded and a filled style (home_rounded,
home_filled, ...). The bottom bar uses them through pds-navigation-item's
icon attribute, which swaps styles with selection:
<pds-navigation-bar>
<pds-navigation-item icon="home" selected>Home</pds-navigation-item>
<pds-navigation-item icon="watchlist">Watchlist</pds-navigation-item>
<pds-navigation-item icon="globe">Markets</pds-navigation-item>
<pds-navigation-item icon="trade">Trade</pds-navigation-item>
<pds-navigation-item icon="community">Community</pds-navigation-item>
<pds-navigation-item icon="me">Me</pds-navigation-item>
</pds-navigation-bar>See src/pds-icon.md and src/pds-navigation-bar.md.
p3-comp-lib/
tokens/tokens.css - colour tokens as CSS custom properties (light + dark)
brand/ - POEMS logo and mark as SVG (website pages only)
icons/ - V2 icon set, 135 SVG files + icons.json (use through pds-icon)
examples/ - reference screens pulled from Figma, with provenance and staleness (examples/README.md)
preview/pds-preview.* - the reviewer's frame and toggles for generated screens (not in index.js)
src/pds-<name>.js - one file per component (25 files)
src/pds-<name>.md - usage guidance per component (when to use, do & don't, gaps)
custom-elements.json - machine-readable manifest of all 25 components
index.js - imports every component in one line
package.json
31 custom elements: Segmented button (pds-segmented-button, with pds-segment), Badge (pds-badge), Button (pds-button), Card
(pds-card, an early exploration, see below), Checkbox (pds-checkbox),
Chip (pds-chip), Dialog (pds-dialog), Divider (pds-divider), List
(pds-list + pds-list-row), Menu (pds-menu + pds-menu-item),
Icon (pds-icon), Icon button (pds-icon-button), Status bar (pds-status-bar), Home indicator (pds-home-indicator), Navigation bar (pds-navigation-bar + pds-navigation-item), Progress indicator
(pds-progress, circular and linear), Radio button (pds-radio), Search
(pds-search), Sheets (pds-bottom-sheet + pds-side-sheet), Snackbar
(pds-snackbar), Switch (pds-switch), Tabs (pds-tabs + pds-tab), Text
field (pds-text-field), Tooltip (pds-tooltip, plain and rich), Top app
bar (pds-top-app-bar).
Card is worth calling out on its own: it has no settled design yet, only an
early proposal, so pds-card is a clearly-labelled starting point built
from that proposal's own numbers, not a finished component like the other
eighteen.
Each of these is a real, deliberate gap, not an oversight:
- Hover and pressed colours are not finalised yet for several components; the washes in those files are a placeholder. Fields have their focus outline (blue-500) from the design system.
- No Singpass mark in the icon set yet; a "Link with Singpass" button
uses
id_carduntil the brand asset is added. - The badge count cap (
max, default99) is left to the caller to override rather than fixed by the library, since the right cap for a given product surface hasn't been settled.
This folder is self-contained; push it as the root of its own repository,
or as a subfolder of an existing one. No node_modules, no build output:
everything here is checked-in source.
MIT terms with one condition added: the library may be used only to
design, prototype, build or test user interfaces for Phillip Securities
products and services, and the brand assets only unaltered on those
interfaces. That covers Phillip's own teams (design, product, business
analysts, developers), vendors and contractors working on those products,
and the AI coding agents any of them use: an agent may read, copy and
build on the library to produce screens for those products, on the same
terms as the person it works for. It is not a general-purpose UI kit, and
it is not an OSI-approved open source licence. Full text in LICENSE.md.
Why does every preview come with a Dark toggle? Because light attracts bugs. 🪲