shelter UI Documentation
shelter UI is a set of UI components designed to look identical to Discord's, built entirely in Solid.
The API signatures are not identical to Discord's React components.
If you are using shelter, these are exposed to you on shelter.ui
, and if not, you can get it at @uwu/shelter-ui
, and please read the relevant docs.
Accessibility
All efforts are taken to support users of accessibility technologies, including screen readers and keyboard navigation.
This entails a few blanket behaviours across shelter-ui:
- All interactive elements (button, link, switch, etc.) have a focus ring
- All interactive elements take an
aria-label
prop to override the default label- Labels are usually sensibly picked, eg the button and switchitem children prop
- Relevant interactive elements (checkboxes, switches, textboxes) take an id prop to use with a
<label>
.
Utils
Not components, but UI utils used in shelter.
<ReactiveRoot>
Type Signature
solid.Component<{ children: JSX.Element }>
ReactiveRoot
creates a solid reactive root, to ensure that onCleanup
works, and fix some reactivity bugs.
elem.append(<ReactiveRoot>{/* ... */}</ReactiveRoot>);
injectCss
Type Signature
(string, Node?) => (string?) => void
injectCss
, as the name says, injects CSS.
It returns a callback which, if passed another string, changes the injected CSS. If this callback is passed undefined
, the CSS is removed from the page. At this point, you cannot pass another string into this callback to re-add some styles.
const modify = injectCss(" .myClass { color: red } ");
modify(" .myClass { color: green } "); // modifies the css
modify(); // removes the css
modify(" .myClass { color: blue } "); // no-op
cleanupCss
standalone only
Type Signature
() => void
Removes all injected css.
genId
Type Signature
() => string
genId
generates a random ID.
This is useful in cases where you need to link elements together by ID, but don't actually need a meaningful ID.
export default () => {
const id = genId();
return (
<>
<label for={id}>A useful input</label>
<input id={id} />
</>
);
};
openModal
Type Signature
(
(() => void) => JSX.Element
) => () => void
openModal
opens the given component in a fullscreen popup modal.
It passes your component one prop, close
, which is a function that closes the modal.
It returns a function that removes your modal.
You can open multiple modals, and focus trapping as well as clicking outside to close are automatic.
const remove = openModal((p) => <button onclick={p}>Hi!</button>);
remove();
TIP
You can use the modal components to style your modals like Discord easily.
TIP
You can listen for your modal being closed using onCleanup.
<ReactInSolidBridge />
shelter only
Type Signature
solid.Component<{ comp: React.ComponentType<TProps>, props: TProps }>
Renders a React component in Solid.
const ComponentFromDiscord = webpack.findByProps("...").default;
<ReactInSolidBridge comp={ComponentFromDiscord} props={{ className: "reactelem", tag: "H1" }} />;
SolidInReactBridge
shelter only
Type Signature
React.ComponentType<{ comp: solid.Component<TProps>, props: TProps }>
Renders a Solid component in React. Using this directly is not recommended as you will need to provide your own React instance.
function SolidCounter(props) {
const [count, setCount] = solid.createSignal(0);
return <button class={props.className} onClick={() => setCount(count() + 1)}>yep {count()}</button>;
}
React.createElement(SolidInReactBridge, {
comp: SolidCounter,
props: {className: "solidelem"},
});
// if you were using React JSX: <SolidInReactBridge comp={SolidCounter} props={{className: "solidelem"}} />
renderSolidInReact
shelter only
Type Signature
(solid.Component<TProps>, TProps) => React.ElementType
Just a wrapper to React.createElement(SolidInReactBridge, {comp, props})
function Component(props) {
const [count, setCount] = solid.createSignal(0);
return <button class={props.className} onClick={() => setCount(count() + 1)}>yep {count()}</button>;
}
// Get a Discord component using React fiber or something
component.render = () => renderSolidInReact(Component, { className: "solidelem" });
<ErrorBoundary />
Type Signature
solid.Component<{ children: JSX.Element }>
Safely catches any errors that occur during rendering, displays the error, and has a button to retry.
showToast
Type Signature
({
title?: string,
content?: string,
onClick?(): void,
class?: string,
duration?: number
}) => () => void
Shows a toast notification. Returns a function to remove it instantly.
The default duration is 3000
.
// all of these props are optional
showToast({
title: "title!",
content: "a cool toast",
onClick() {},
class: "my-cool-toast",
duration: 3000,
});
initToasts
standalone only
Type Signature
() => void
Sets up necessary things for toasts to work. You must call this if you are using toasts and are not using shelter.
niceScrollbarsClass
Type Signature
() => string
A getter that gets a class to add to an element to give it a Discord-style scrollbar.
<div class={`myclass myclass2 ${niceScrollbarsClass()}`} />
use:focusring
Adds a visible ring around your element when focused via keyboard (tab key), to aid with accessibility.
Optionally takes a border radius.
focusring
must be in scope, either via an import from shelter-ui, or otherwise (e.g. const { focusring } = shelter.ui
).
WARNING
Be careful - some tooling incorrectly tree shakes this if imported using ESM. You can use false && focusring
to prevent the tree shaking (it will be minified away). If getting via destructuring in shelter, you should be fine.
The focusring is included for you on interactive shelter-ui components, so if you are using those, this is not necessary.
<button use:focusring>do a thing</button>
<input use:focusring={6} type="checkbox" />
use:tooltip
Shows a Discord-style tooltip when you hover over the element.
Same scope rules apply as focusring.
You can pass any JSX element type, including strings and elements.
If you pass undefined, it will do nothing.
You can pass an of the form [true, JSX.Element]
to render it underneath instead of on top of the element.
<button use:tooltip="Delete"><DeleteIcon /></button>
<button use:tooltip={[true, "Delete but underneath"]}><DeleteIcon /></button>
<Space />
Type Signature
solid.Component
A spacebar character that will never be collapsed out of your JSX. Useful in flexboxes etc.
openConfirmationModal
Type Signature
({
body: solid.Component,
header: solid.Component,
confirmText?: string,
cancelText?: string,
type?: ModalTypes,
size?: string,
}) => Promise<void>
openConfirmationModal
shows a premade modal with a header, body, and confirm and cancel buttons.
It returns a promise that resolves on confirm, and rejects on cancel or close.
openConfirmationModal({
header: () => "Are you sure????",
body: () => "Really destroy your entire hard disk? You sure? What?",
type: "danger",
confirmText: "Yes, really.",
cancelText: "Oh shoot!"
}).then(
() => console.log("let's delete!"),
() => console.log("chicken.")
);
Components
<Text>
Type Signature
solid.Component<{ children: JSX.Element }>
Text just renders some text, using Discord's current text colour, instead of just black or whatever.
<Text>This is some text</Text>
<Header>
Type Signature
solid.Component<{
tag: string,
children: JSX.Element,
class?: string,
id?: string
}>
Header is, well, a header. It has a few styles, chosen by the tag
prop.
<Header tag={HeaderTags.H1}>My cool page</Header>
HeaderTags
HeaderTags.H1
: A nice big header - like the ones at the top of user settings sections.HeaderTags.H2
: A slightly smaller header, with allcaps text.HeaderTags.H3
: A smaller again header - like "Gifts you purchased" in settings.HeaderTags.H4
: Smaller again, allcaps text.HeaderTags.H5
: Small, allcaps text, default - like "sms backup authentication" in settings.HeaderTags.EYEBROW
: A descriptive keyword or phrase placed above the main headline.
<Divider />
Type Signature
solid.Component<{ mt?: true | string, mb?: true | string }>
Divider renders a grey horizontal divider line.
The mt
and mb
props control the top and bottom margin.
By default, there are no margins. When a string is provided, that is the margin value. When set true, 20px
is used.
<Divider mt mb="30px" /> // 20px on top, 30px on bottom.
<Button>
Type Signature
solid.Component<{
look?: string, // default ButtonLooks.FILLED
color?: ButtonColor, // default ButtonColors.BRAND
size?: ButtonSize, // default ButtonSizes.SMALL
grow?: boolean, // default false, adds width: auto
disabled?: boolean,
type?: "button" | "reset" | "submit",
style?: JSX.CSSProperties,
class?: string,
onClick?(): void,
onDoubleClick?(): void,
children?: JSX.Element
tooltip?: string
}>
Button is a, well, button, using Discord's styles.
ButtonLooks
Type: Record<string, string>
ButtonLooks.FILLED
: the standard Discord button like you see all over user settingsButtonLooks.INVERTED
: swaps the bg and fg coloursButtonLooks.OUTLINED
: only outline and text is coloured, until hover when bg fills inButtonLooks.LINK
: removes the background and unsets the colour - looks like text!
ButtonColors
Type: Record<string, ButtonColor>
ButtonColors.BRAND
: blurple / primary buttonButtonColors.RED
ButtonColors.GREEN
ButtonColors.SECONDARY
: gray backgroundButtonColors.LINK
: blue like a proper<a>
-style link.ButtonColors.WHITE
: looks invertedButtonColors.BLACK
ButtonColors.TRANSPARENT
ButtonSizes
Type: Record<string, ButtonSize>
ButtonSizes.NONE
: does not attempt to set sizing on the buttonButtonSizes.TINY
: 53x24ButtonSizes.SMALL
: 50x32ButtonSizes.MEDIUM
: 96x38ButtonSizes.LARGE
: 130x44ButtonSizes.XLARGE
: 148x50, increases font size & paddingButtonSizes.MIN
: as small as the content allows, increases padding,display: inline
ButtonSizes.MAX
: as large as the container allows, increases font sizeButtonSizes.ICON
: unset width, as high as the container allows, increases padding
<LinkButton>
Type Signature
solid.Component<{
style?: JSX.CSSProperties,
class?: string,
href?: string,
"aria-label"?: string,
tooltip?: string,
children?: JSX.Element
}>
A link (<a>
) that fits with Discord's UI.
It will open the href in a new tab / in your system browser.
<Switch />
Type Signature
solid.Component<{
id?: string,
checked?: boolean,
disabled?: boolean,
onChange?(boolean): void,
tooltip?: JSX.Element,
"aria-label"?: string
}>
A toggle switch.
The id
prop sets the id of the <input>
.
checked
, disabled
, onChange
should be pretty self-explanatory.
tooltip
, if set, adds a tooltip.
const [switchState, setSwitchState] = createSignal(false);
<Switch checked={switchState} onChange={setSwitchState} />;
<SwitchItem>
Type Signature
solid.Component<{
value: boolean,
disabled?: boolean,
onChange?(boolean): void,
children: JSX.Element,
note?: JSX.Element,
hideBorder?: boolean,
tooltip?: JSX.Element,
"aria-label"?: string
}>
An item with an option name, a switch, and optionally some extra info.
value
sets the value of the switch, disabled
and onChange
work as you'd expect.
note
, if passed, sets the extra info to be displayed under the title and switch.
Unless hideBorder
is set, a <Divider />
is rendered under the component.
The child elements of the component is the title displayed next to the switch.
tooltip
, if set, adds a tooltip.
<SwitchItem note="Does cool things" value={/*...*/}>A cool option</SwitchItem>
<Checkbox />
Type Signature
solid.Component<{
id?: string,
checked?: boolean,
disabled?: boolean,
onChange?(boolean): void,
tooltip?: JSX.Element,
"aria-label"?: string
}>
Like <Switch />
but its a checkbox.
<CheckboxItem>
Type Signature
solid.Component<{
checked: boolean,
disabled?: boolean,
onChange?(boolean): void,
children: JSX.Element,
mt?: boolean,
tooltip?: JSX.Element,
"aria-label"?: string
}>
Like <SwitchItem>
but its a checkbox. Takes an extra mt
prop which enables a top margin. No note or divider.
Modal Components
Components for Discord-styled modals. Also see openModal()
<ModalRoot size={ModalSizes.SMALL}>
<ModalHeader /* noClose */ close={closeFn}>My cool modal</ModalHeader>
<ModalBody>Look mom! I'm on the shelter-ui modal!</ModalBody>
<ModalFooter>Uhhhhh idk this is the footer ig, its a good place for buttons!</ModalFooter>
</ModalRoot>
<ModalRoot>
Type Signature
solid.Component<{
size?: string,
children?: JSX.Element,
class?: string,
style?: JSX.CSSProperties | string
}>
The root component of a discord-styled modal.
Takes a size
from ModalSizes
and some child elements.
size
defaults to ModalSizes.SMALL
.
All provided child parts of the modal (header, body, footer) are optional.
ModalSizes
Type: Record<string, string>
ModalSizes.SMALL
: 440 wide, 200~720 tallModalSizes.MEDIUM
: 600 wide, 400~800 tall
<ModalHeader>
Type: solid.Component<{ noClose?: boolean, close(): void, children?: JSX.Element }>
The header of a discord-styled modal.
Takes a prop, close
, which is the function that closes the modal.
Also has an optional boolean prop noClose
which hides the close button.
<ModalBody>
Type: solid.Component<{ children?: JSX.Element }>
The body of a discord-styled modal.
Has nice discord scrollbars and plays well with the header and footer when overflowed.
<ModalFooter>
Type: solid.Component<{ children: JSX.Element }>
The footer of a Discord-styled modal, good for buttons!
<ModalConfirmFooter />
Type Signature
solid.Component<{
close(): void,
confirmText?: string, // default "Confirm"
cancelText?: string, // default "Cancel"
type?: "neutral" | "danger" | "confirm", // default "neutral"
onConfirm?(): void,
onCancel?(): void,
disabled?: boolean,
cancelDisabled?: boolean
}>
A modal footer with configurable confirm and cancel buttons, the most common type of modal footer.
The type
prop controls the colour of the confirm button.
The disabled
prop affects the confirm button, and the cancelDisabled
prop affects the cancel button.
<TextBox />
Type Signature
solid.Component<{
value?: string,
placeholder?: string,
maxLength?: number,
id?: string,
"aria-label"?: string,
onInput?(string): void
}>
A discord style textbox.
Takes value, placeholder, maxLength, and onInput.
All optional. onInput called every keystroke and passed the full current value.
<TextArea />
Type Signature
solid.Component<{
value?: string,
placeholder?: string,
maxLength?: number,
id?: string,
"aria-label"?: string,
onInput?(string): void
width?: string,
height?: string,
"resize-x"?: boolean,
"resize-y"?: boolean,
mono?: boolean
}>
Like <TextBox />
but its a textarea.
The size can be set, user resizing can be toggled, and you can apply a monospace font.
<Slider />
Type Signature
solid.Component<{
min: number,
max: number,
tick?: boolean | number,
step?: number | "any",
class?: string,
style?: JSX.CSSProperties,
onInput?(number): void,
value?: number
}>
A discord-style slider. Takes value
and returns in onInput
as number,
Set min
and max
as needed.
step
controls the size of the actual steps the slider is locked to.
tick
controls the spacing between ticks to show. This must be an even divisor of (min - max), but plans are to fix this in the future.
If tick
is not passed, no ticks show.
step
is any by default.
<Slider value={val()} onInput={setVal}
min={0} max={10}
step={2} tick
/>
Standalone usage
shelter UI can be used outside of Discord. There are a few things that must be taken into consideration for this:
- components may be affected by lack of Discord's stylesheets (css reset, etc.)
- components may be affected by your own stylesheets
- you must use the compatibility stylesheet
- you must inject the shelter UI internal styles
- you must be using Solid, or have some way of using Solid components in your page
- some APIs available on
shelter.ui
are not available in@uwu/shelter-ui
, and vice versa.
To get started in your Solid app/site, install @uwu/shelter-ui
. It is a rolling release package, not semver, the version numbers simply increment each release. This is because shelter itself is rolling release and has no versioning.
You must inject @uwu/shelter-ui/compat.css
somewhere in your app. This does two things. First, it sets CSS variables on :root
that the components rely on to look correct. The reason we use these variables at all instead of hardcoding their values is so that themes apply to shelter UI inside of Discord. Second, it adds @font-face
rules for the custom fonts used by Discord, so that relevant fonts (gg sans, etc) load correctly.
If you are using shelter UI within your main page, you should then import and call injectInternalStyles()
. This will add a style tag to your page to make the components look correct. If you want to use toasts, you must call initToasts()
.
If you are using shelter UI within a shadow root or some other isolated context where this won't work, you should import and render <InternalStyles />
somewhere within the relevant context.
APIs that are only available in shelter will have this badge next to them in these docs:
They are also listed here:
shelter-only:
ReactInSolidBridge
SolidInReactBridge
renderSolidInReact
standalone-only:
cleanupCss
initToasts